Skip to content

Commit

Permalink
Merge pull request #330 from LimoWanKenobi/links_to_other_types
Browse files Browse the repository at this point in the history
[WIP] Adds methods for cross-type links
  • Loading branch information
matthid committed Oct 14, 2015
2 parents d810b17 + 8e18caa commit 044c3d5
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 5 deletions.
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 = ()

0 comments on commit 044c3d5

Please sign in to comment.