Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds methods for cross-type links #330

Merged
merged 15 commits into from
Oct 14, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/content/metadata.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,55 @@ MetadataFormat.Generate
sourceRepo = "https://github.com/tpetricek/FSharp.Formatting/tree/master",
sourceFolder = "/path/to/FSharp.Formatting" )


(**
Adding cross-type links to modules and types in the same assembly
-----------------
You can automatically add cross-type links to the documentation pages of other modules and types in the same assembly.
You can do this in two different ways:
* Add a [markdown inline link](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#links) were the link
title is the name of the type you want to link.


/// this will generate a link to [Foo.Bar] documentation


* Add a [Markdown inlide code](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code) (using
back-ticks) where the code is the name of the type you want to link.


/// This will also generate a link to `Foo.Bar` documentation


You can use either the full name (including namespace and module) or the simple name of a type.
If more than one type is found with the same name the link will not be generated.
If a type with the given name is not found in the same assembly the link will not be generated.
*)

/// Contains two types [Bar] and [Foo.Baz]
module Foo =

/// Bar is just an `int` and belongs to module [Foo]
type Bar = int

/// Baz contains a `Foo.Bar` as its `id`
type Baz = { id: Bar }

/// This function operates on `Baz` types.
let f (b:Baz) =
b.id * 42

/// Referencing [Foo3] will not generate a link as there is no type with the name `Foo3`
module Foo2 =

/// This is not the same type as `Foo.Bar`
type Bar = double

/// Using the simple name for [Bar] will fail to create a link because the name is duplicated in
/// [Foo.Bar] and in [Foo2.Bar]. In this case, using the full name works.
let f2 b =
b * 50

(**
Excluding APIs from the docs
-----------------
Expand Down
83 changes: 82 additions & 1 deletion src/FSharp.MetadataFormat/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,73 @@ module Reader =
let str = full.ToString()
Comment.Create(str, str, [KeyValuePair("<default>", str)])

/// Returns all indirect links in a specified span node
let rec collectSpanIndirectLinks span = seq {
match span with
| IndirectLink (_, _, key) -> yield key
| Matching.SpanLeaf _ -> ()
| Matching.SpanNode(_, spans) ->
for s in spans do yield! collectSpanIndirectLinks s }

/// Returns all indirect links in the specified paragraph node
let rec collectParagraphIndirectLinks par = seq {
match par with
| Matching.ParagraphLeaf _ -> ()
| Matching.ParagraphNested(_, pars) ->
for ps in pars do
for p in ps do yield! collectParagraphIndirectLinks p
| Matching.ParagraphSpans(_, spans) ->
for s in spans do yield! collectSpanIndirectLinks s }

/// Returns whether the link is not included in the document defined links
let linkNotDefined (doc: LiterateDocument) (link:string) =
[ link; link.Replace("\r\n", ""); link.Replace("\r\n", " ");
link.Replace("\n", ""); link.Replace("\n", " ") ]
|> Seq.map (fun key -> not(doc.DefinedLinks.ContainsKey(key)) )
|> Seq.reduce (fun a c -> a && c)

/// Returns a tuple of the undefined link and its Cref if it exists
let getTypeLink (ctx:ReadingContext) undefinedLink =
// Append 'T:' to try to get the link from urlmap
match ctx.UrlMap.ResolveCref ("T:" + undefinedLink) with
| Some cRef -> if cRef.IsInternal then Some (undefinedLink, cRef) else None
| None -> None

/// Adds a cross-type link to the document defined links
let addLinkToType (doc: LiterateDocument) link =
match link with
| Some(k, v) -> do doc.DefinedLinks.Add(k, (v.ReferenceLink, Some v.NiceName))
| None -> ()

/// Wraps the span inside an `IndirectLink` if it is an inline code that can be converted to a link
let wrapInlineCodeLinksInSpans (ctx:ReadingContext) span =
match span with
| InlineCode(code) ->
match getTypeLink ctx code with
| Some _ -> IndirectLink([span], code, code)
| None -> span
| _ -> span

/// Wraps inside an `IndirectLink` all inline code spans in the paragraph that can be converted to a link
let rec wrapInlineCodeLinksInParagraphs (ctx:ReadingContext) (para:MarkdownParagraph) =
match para with
| Matching.ParagraphLeaf _ -> para
| Matching.ParagraphNested(info, pars) ->
Matching.ParagraphNested(info, pars |> List.map (fun innerPars -> List.map (wrapInlineCodeLinksInParagraphs ctx) innerPars))
| Matching.ParagraphSpans(info, spans) -> Matching.ParagraphSpans(info, List.map (wrapInlineCodeLinksInSpans ctx) spans)

/// Adds the missing links to types to the document defined links
let addMissingLinkToTypes ctx (doc: LiterateDocument) =
let replacedParagraphs = doc.Paragraphs |> List.map (wrapInlineCodeLinksInParagraphs ctx)

do replacedParagraphs
|> Seq.collect collectParagraphIndirectLinks
|> Seq.filter (linkNotDefined doc)
|> Seq.map (getTypeLink ctx)
|> Seq.iter (addLinkToType doc)

LiterateDocument(replacedParagraphs, doc.FormattedTips, doc.DefinedLinks, doc.Source, doc.SourceFile, doc.Errors)

let readCommentAndCommands (ctx:ReadingContext) xmlSig =
match ctx.XmlMemberLookup(xmlSig) with
| None ->
Expand Down Expand Up @@ -597,6 +664,7 @@ module Reader =
Literate.ParseMarkdownString
( text, path=Path.Combine(ctx.AssemblyPath, "docs.fsx"),
formatAgent=ctx.FormatAgent, compilerOptions=ctx.CompilerOptions )
|> (addMissingLinkToTypes ctx)
cmds :> IDictionary<_, _>, readMarkdownComment doc
else
lines
Expand Down Expand Up @@ -693,6 +761,7 @@ module Reader =
let usedNames = Dictionary<_, _>()
let registeredEntities = Dictionary<_, _>()
let entityLookup = Dictionary<_, _>()
let niceNameEntityLookup = Dictionary<_, _>()
let nameGen (name:string) =
let nice = (toReplace
|> Seq.fold (fun (s:string) (inv, repl) -> s.Replace(inv, repl)) name)
Expand All @@ -710,6 +779,9 @@ module Reader =
if (not (System.String.IsNullOrEmpty xmlsig)) then
assert (xmlsig.StartsWith("T:"))
entityLookup.[xmlsig.Substring(2)] <- entity
if (not(niceNameEntityLookup.ContainsKey(entity.LogicalName))) then
niceNameEntityLookup.[entity.LogicalName] <- List<_>()
niceNameEntityLookup.[entity.LogicalName].Add(entity)
for nested in entity.NestedEntities do registerEntity nested

let getUrl (entity:FSharpEntity) =
Expand Down Expand Up @@ -738,7 +810,16 @@ module Reader =
match entityLookup.TryGetValue(typeName) with
| true, entity ->
Some { IsInternal = true; ReferenceLink = sprintf "%s.html" (getUrl entity); NiceName = entity.LogicalName }
| _ -> None
| _ ->
match niceNameEntityLookup.TryGetValue(typeName) with
| true, entityList ->
if entityList.Count = 1 then
Some{ IsInternal = true; ReferenceLink = sprintf "%s.html" (getUrl entityList.[0]); NiceName = entityList.[0].LogicalName }
else
if entityList.Count > 1 then
do Log.warnf "Duplicate types found for the simple name: %s" typeName
None
| _ -> None

let resolveCref (cref:string) =
if (cref.Length <= 2) then invalidArg "cref" (sprintf "the given cref: '%s' is invalid!" cref)
Expand Down
57 changes: 55 additions & 2 deletions tests/FSharp.MetadataFormat.Tests/Tests.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if INTERACTIVE
#if INTERACTIVE
#I "../../bin"
#r "FSharp.MetadataFormat.dll"
#r "FSharp.Compiler.Service.dll"
Expand Down Expand Up @@ -452,4 +452,57 @@ let ``MetadataFormat test FsLib1``() =

let files =
dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ]
files.ContainsKey "fslib-test_omit.html" |> should equal false
files.ContainsKey "fslib-test_omit.html" |> should equal false

// -------------------Indirect links----------------------------------
[<Test>]
let ``Metadata generates cross-type links for Indirect Links``() =
let library = root @@ "files/FsLib/bin/Debug" @@ "FsLib2.dll"
let output = getOutputDir()
MetadataFormat.Generate([library], output, layoutRoots, info, libDirs = [root @@ "../../lib"], markDownComments = true)
let fileNames = Directory.GetFiles(output)
let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ]

// Check that a link to MyType exists when using Full Name of the type
files.["fslib-nested.html"] |> should contain "This function returns a <a href=\"fslib-nested-mytype.html\" title=\"MyType\">FsLib.Nested.MyType</a>"

// Check that a link to OtherType exists when using Logical Name of the type only
files.["fslib-nested.html"] |> should contain "This function returns a <a href=\"fslib-nested-othertype.html\" title=\"OtherType\">OtherType</a>"

// Check that a link to a module is created when using Logical Name only
files.["fslib-duplicatedtypename.html"] |> should contain "This type name will be duplicated in <a href=\"fslib-nested.html\" title=\"Nested\">Nested</a>"

// Check that a link to a type with a duplicated name is created when using full name
files.["fslib-nested-duplicatedtypename.html"] |> should contain "This type has the same name as <a href=\"fslib-duplicatedtypename.html\" title=\"DuplicatedTypeName\">FsLib.DuplicatedTypeName</a>"

// Check that a link to a type with a duplicated name is not created when using Logical name only
files.["fslib-nested.html"] |> should contain "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"] |> should contain "This function returns a [InexistentTypeName] multiplied by 5."

// -------------------Inline code----------------------------------
let ``Metadata generates cross-type links for Inline Code``() =
let library = root @@ "files/FsLib/bin/Debug" @@ "FsLib2.dll"
let output = getOutputDir()
MetadataFormat.Generate([library], output, layoutRoots, info, libDirs = [root @@ "../../lib"], markDownComments = true)
let fileNames = Directory.GetFiles(output)
let files = dict [ for f in fileNames -> Path.GetFileName(f), File.ReadAllText(f) ]

// Check that a link to MyType exists when using Full Name of the type in a inline code
files.["fslib-nested.html"] |> should contain "You will notice that <a href=\"fslib-nested-mytype.html\" title=\"MyType\"><code>FsLib.Nested.MyType</code></a> is just an <code>int</code>"

// Check that a link to MyType exists when using Full Name of the type in a inline code
files.["fslib-nested.html"] |> should contain "You will notice that <a href=\"fslib-nested-othertype.html\" title=\"OtherType\"><code>OtherType</code></a> is just an <code>int</code>"

// Check that a link to a type with a duplicated name is not created when using Logical name only
files.["fslib-nested.html"] |> should contain "<code>DuplicatedTypeName</code> is duplicated so it should no add a cross-type link"

// Check that a link to a type with a duplicated name is not created when using Logical name only
files.["fslib-nested.html"] |> should contain "<code>InexistentTypeName</code> 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"] |> should contain "This type name will be duplicated in <a href=\"fslib-nested.html\" title=\"Nested\"><code>Nested</code></a>"

// Check that a link to a type with a duplicated name is created when using full name
files.["fslib-nested-duplicatedtypename.html"] |> should contain "This type has the same name as <a href=\"fslib-duplicatedtypename.html\" title=\"DuplicatedTypeName\"><code>FsLib.DuplicatedTypeName</code></a>"
34 changes: 32 additions & 2 deletions tests/FSharp.MetadataFormat.Tests/files/FsLib/Library2.fs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
namespace FsLib
namespace FsLib

/// This type name will be duplicated in [Nested]
/// This type name will be duplicated in `Nested`
type DuplicatedTypeName = int

/// Sample class
type Class() =
Expand All @@ -22,6 +26,32 @@ module Nested =
/// Very nested member
member x.Member = ""

/// This is My type
type MyType = int

/// This is other type
type OtherType = int

/// This type has the same name as [FsLib.DuplicatedTypeName]
/// This type has the same name as `FsLib.DuplicatedTypeName`
type DuplicatedTypeName = int

/// This function returns a [FsLib.Nested.MyType] multiplied by 2.
/// You will notice that `FsLib.Nested.MyType` is just an `int`
let f x :MyType = x * 2

/// This function returns a [OtherType] multiplied by 3.
/// You will notice that `OtherType` is just an `int`
let f2 x :OtherType = x * 3

/// This function returns a [DuplicatedTypeName] multiplied by 4.
/// `DuplicatedTypeName` is duplicated so it should no add a cross-type link
let f3 x :OtherType = x * 4

/// This function returns a [InexistentTypeName] multiplied by 5.
/// `InexistentTypeName` does not exists so it should no add a cross-type link
let f4 x :OtherType = x * 5

type ITest_Issue229 = abstract member Name : string

type Test_Issue229 (name) =
Expand Down Expand Up @@ -59,4 +89,4 @@ module Test_Issue201Extensions =
/// [omit]
type Test_Omit() =
/// This Should not be displayed
member x.Foo a = ()
member x.Foo a = ()