From f826333173f7818f7ef07b4f30c134798c610bb6 Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Sun, 7 Jun 2020 11:42:15 +0100 Subject: [PATCH 01/10] Get initial rough cut into source control --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 230 +++++++++++++++++- .../XmlDocMarkdown.Core.csproj | 5 + 2 files changed, 228 insertions(+), 7 deletions(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index fb098259..3581da37 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -5,9 +5,14 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Net; using System.Reflection; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices.ComTypes; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; namespace XmlDocMarkdown.Core @@ -605,14 +610,37 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea if (typeInfo != null && declaringType == null && !string.IsNullOrEmpty(context.SourceCodePath) && !string.IsNullOrEmpty(context.RootNamespace)) { - string namespaceName = GetNamespaceName(typeInfo); - if (namespaceName.StartsWith(context.RootNamespace, StringComparison.Ordinal)) + var documents = context.MetadataContext[typeInfo.FullName]; + if (documents.Any()) { - string directoryPath = context.SourceCodePath + namespaceName.Substring(context.RootNamespace.Length).Replace('.', '/'); - if (!Uri.TryCreate(directoryPath, UriKind.Absolute, out _)) - directoryPath = "../" + directoryPath; - string fileName = GetShortName(typeInfo) + ".cs"; - writer.WriteLine($"* [{fileName}]({directoryPath}/{fileName})"); + foreach (var document in documents) + { + var fileName = Path.GetFileName(document); + if (context.MetadataContext.TrySourceLink(document, out var link)) + { + writer.WriteLine($"* [{fileName}]({link})"); + } + else + { + var snip = document.Substring(context.MetadataContext.PrefixLength); + string filePath = context.SourceCodePath + snip.Replace('\\', '/'); + if (!Uri.TryCreate(filePath, UriKind.Absolute, out _)) + filePath = "../" + filePath; + writer.WriteLine($"* [{fileName}]({filePath})"); + } + } + } + else // default to old behaviour if .PDB cannot be read + { + string namespaceName = GetNamespaceName(typeInfo); + if (namespaceName.StartsWith(context.RootNamespace, StringComparison.Ordinal)) + { + string directoryPath = context.SourceCodePath + namespaceName.Substring(context.RootNamespace.Length).Replace('.', '/'); + if (!Uri.TryCreate(directoryPath, UriKind.Absolute, out _)) + directoryPath = "../" + directoryPath; + string fileName = GetShortName(typeInfo) + ".cs"; + writer.WriteLine($"* [{fileName}]({directoryPath}/{fileName})"); + } } } @@ -2170,6 +2198,10 @@ public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary> typemap = + new Dictionary>(); + + private Dictionary sourcelink = + new Dictionary(); + + public MetadataContext() + { + PrefixLength = 0; + } + + public MetadataContext(string assemblyPath) + { + var index = 0; + using (var stream = File.OpenRead(assemblyPath)) + using (var reader = new PEReader(stream)) + { + Func streamProvider = p => new FileStream(p, FileMode.Open, FileAccess.Read); + + var metadata = reader.GetMetadataReader(MetadataReaderOptions.ApplyWindowsRuntimeProjections); + var pdbLoaded = reader.TryOpenAssociatedPortablePdb(stream.Name, streamProvider, out var metadataReaderProvider, + out var pdbPath); + + if (pdbLoaded) + { + var metadataSymbol = metadataReaderProvider.GetMetadataReader(); + + // Load all the file paths + var names = metadataSymbol.Documents + .Select(metadataSymbol.GetDocument) + .Select(d => metadataSymbol.GetString(d.Name)) + .Distinct() + .ToList(); + + // identify the common prefix + var shortest = names.OrderBy(s => s.Length).First(); + for (; index < shortest.Length; ++index) + { + var c = shortest[index]; + var match = names.All(n => n[index] == c); + if (!match) + break; + } + + // for each type, identify the file(s) it refers to + var types = metadata.TypeDefinitions.Select(metadata.GetTypeDefinition); + + typemap = types.ToDictionary(t => TypeName(metadata, t), + t => t.GetMethods().Select(m => metadataSymbol.GetMethodDebugInformation(m)) + .SelectMany(a => a.GetSequencePoints()) + .Select(a => metadataSymbol.GetDocument(a.Document)) + .Select(d => metadataSymbol.GetString(d.Name)) + .Distinct() + .ToList() + ); + + // identify sourcelink data, if present + var custom = metadataSymbol.CustomDebugInformation + .Select(m => metadataSymbol.GetCustomDebugInformation(m)) + .Where(c => metadataSymbol.GetGuid(c.Kind) == + // magic ID -- https://github.com/dotnet/corefx/blob/master/src/System.Reflection.Metadata/specs/PortablePdb-Metadata.md#source-link-c-and-vb-compilers + // It works for F# too, haven't tried C++/CLI + Guid.Parse("cc110556-a091-4d38-9fec-25ab9a351a6a")) + .Select(c => metadataSymbol.GetBlobBytes(c.Value)) + .FirstOrDefault(); + + if (custom != null) + { + // Expect the blob to be well-formed + using (var blob = new MemoryStream(custom)) + { + var json = JsonDocument.Parse(blob); + var root = json.RootElement; + var documents = root.GetProperty("documents"); + using (var scan = documents.EnumerateObject()) + { + foreach (var item in scan) + { + sourcelink.Add( + item.Name, + item.Value.GetString() + ); + } + } + } + } + } + } + + PrefixLength = index; + } + + public int PrefixLength { get; } + + public IEnumerable this[string typename] => + typemap.TryGetValue(typename, out var documents) ? + new ReadOnlyCollection(documents) : + Enumerable.Empty(); + + public bool TrySourceLink(string filepath, out string link) + { + if (sourcelink.TryGetValue(filepath, out link)) + { + return true; + } + + return TryLocateMatch(filepath, sourcelink, out link); + } + + private static bool TryLocateMatch(string file, + Dictionary dict, out string match) + { + if (TryFindClosestMatch(file, dict, out var best, out var relative)) + { + var replacement = + Path.Combine(relative, Path.GetFileName(file)).Replace('\\', '/'); + var url = dict[best].Replace("*", replacement); + dict.Add(file, url); + match = url; + return true; + } + match = file; + return false; + } + + private static bool TryFindClosestMatch(string file, + Dictionary dict, out string best, + out string relative) + { + var unmapped = dict.Keys.Where(k => Path.GetFileName(k) == "*"); + var dir = Path.GetDirectoryName(file); + + var candidate = + unmapped.Select(x => new + { + Best = x, + Relative = GetRelativePath(Path.GetDirectoryName(x), dir) + }) + .Where(m => m.Relative.IndexOf("..", StringComparison.Ordinal) < 0) + .OrderBy(m => m.Relative.Length) + .FirstOrDefault(); + + best = candidate?.Best; + relative = candidate?.Relative; + return candidate != null; + } + + private static string EnsureEndsWith(string s, string c) + { + return s.EndsWith(c, StringComparison.Ordinal) ? + s : s + c; + } + + private static string GetRelativePath(string relativeTo, string path) + { + if (Path.GetFullPath(path) == Path.GetFullPath(relativeTo)) + { + return String.Empty; + } + + Func ender = s => EnsureEndsWith(s, Path.DirectorySeparatorChar.ToString()); + + var uri = new Uri(ender(relativeTo)); + + return Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()) + .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + private static string TypeName(MetadataReader metadata, TypeDefinition t) + { + var name = metadata.GetString(t.Name); + if (t.IsNested) + return TypeName(metadata, metadata.GetTypeDefinition(t.GetDeclaringType())) + + "+" + name; + + return metadata.GetString(t.Namespace) + "." + metadata.GetString(t.Name); + } } static readonly HashSet s_keywords = new HashSet diff --git a/src/XmlDocMarkdown.Core/XmlDocMarkdown.Core.csproj b/src/XmlDocMarkdown.Core/XmlDocMarkdown.Core.csproj index f519460c..305a8ae6 100644 --- a/src/XmlDocMarkdown.Core/XmlDocMarkdown.Core.csproj +++ b/src/XmlDocMarkdown.Core/XmlDocMarkdown.Core.csproj @@ -8,4 +8,9 @@ true + + + + + From 7fed21e4664977c5980b97d4dccc51650d13c70e Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Sun, 7 Jun 2020 12:47:31 +0100 Subject: [PATCH 02/10] After initial testing --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 26 ++++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index 3581da37..f6dab245 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -100,7 +100,7 @@ private IEnumerable DoGenerateOutput(Assembly assembly, XmlDocAssembl string rootNamespace = RootNamespace ?? visibleNamespaceRecords.OrderBy(x => x.Namespace.Length).ThenByDescending(x => x.Types.Count).Select(x => x.Namespace).FirstOrDefault(x => x.Length != 0); RootPageLocation = $"{safeAssemblyName}" + (PermalinkPretty ? "Assembly.md" : ".md"); - var context = new MarkdownContext(xmlDocAssembly, membersByXmlDocName, assemblyFileName, sourceCodePath, rootNamespace, RootPageLocation); + var context = new MarkdownContext(xmlDocAssembly, membersByXmlDocName, assemblyFileName, sourceCodePath, rootNamespace, RootPageLocation, assembly.Location); yield return CreateNamedText(context.PageLocation, null, assemblyName, writer => { var front = GetFrontMatter(assemblyName, $"{safeAssemblyName}" + (PermalinkPretty ? "Assembly" : "") + extension); @@ -610,9 +610,16 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea if (typeInfo != null && declaringType == null && !string.IsNullOrEmpty(context.SourceCodePath) && !string.IsNullOrEmpty(context.RootNamespace)) { - var documents = context.MetadataContext[typeInfo.FullName]; - if (documents.Any()) + if (context.MetadataContext.PdbLoaded) { + // **Note** Interface types will not be found + + // Allow for F# modules that contain only types + var documents = (new[] { typeInfo.FullName }).Concat( + typeInfo.DeclaredNestedTypes.Select(t => t.FullName)) + .SelectMany(n => context.MetadataContext[n.Replace("+", ".")]) + .Distinct(); + foreach (var document in documents) { var fileName = Path.GetFileName(document); @@ -2190,7 +2197,7 @@ private IEnumerable ToMarkdown(IReadOnlyList blocks, Markdo private class MarkdownContext { - public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary membersByXmlDocName, string assemblyFileName, string sourceCodePath, string rootNamespace, string pageLocation) + public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary membersByXmlDocName, string assemblyFileName, string sourceCodePath, string rootNamespace, string pageLocation, string assemblyLocation) { XmlDocAssembly = xmlDocAssembly; MembersByXmlDocName = membersByXmlDocName; @@ -2201,7 +2208,7 @@ public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary streamProvider = p => new FileStream(p, FileMode.Open, FileAccess.Read); var metadata = reader.GetMetadataReader(MetadataReaderOptions.ApplyWindowsRuntimeProjections); - var pdbLoaded = reader.TryOpenAssociatedPortablePdb(stream.Name, streamProvider, out var metadataReaderProvider, + PdbLoaded = reader.TryOpenAssociatedPortablePdb(stream.Name, streamProvider, out var metadataReaderProvider, out var pdbPath); - if (pdbLoaded) + if (PdbLoaded) { var metadataSymbol = metadataReaderProvider.GetMetadataReader(); @@ -2341,6 +2349,8 @@ public MetadataContext(string assemblyPath) public int PrefixLength { get; } + public bool PdbLoaded { get; } + public IEnumerable this[string typename] => typemap.TryGetValue(typename, out var documents) ? new ReadOnlyCollection(documents) : @@ -2420,7 +2430,7 @@ private static string TypeName(MetadataReader metadata, TypeDefinition t) var name = metadata.GetString(t.Name); if (t.IsNested) return TypeName(metadata, metadata.GetTypeDefinition(t.GetDeclaringType())) + - "+" + name; + "." + name; return metadata.GetString(t.Namespace) + "." + metadata.GetString(t.Name); } From 65040800240d265f8c8fb4c6ae3d32bc433507bb Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Sun, 7 Jun 2020 13:06:34 +0100 Subject: [PATCH 03/10] Generate the nested type names the way that TypeInfo displays them, rather than fixing up post hoc --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index f6dab245..cc5b398d 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -617,7 +617,7 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea // Allow for F# modules that contain only types var documents = (new[] { typeInfo.FullName }).Concat( typeInfo.DeclaredNestedTypes.Select(t => t.FullName)) - .SelectMany(n => context.MetadataContext[n.Replace("+", ".")]) + .SelectMany(n => context.MetadataContext[n]) .Distinct(); foreach (var document in documents) @@ -2430,7 +2430,7 @@ private static string TypeName(MetadataReader metadata, TypeDefinition t) var name = metadata.GetString(t.Name); if (t.IsNested) return TypeName(metadata, metadata.GetTypeDefinition(t.GetDeclaringType())) + - "." + name; + "+" + name; return metadata.GetString(t.Namespace) + "." + metadata.GetString(t.Name); } From 08241ecc5a7cec131ebc881bf96bbe930b39520f Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Sun, 7 Jun 2020 13:39:56 +0100 Subject: [PATCH 04/10] Adjust comment --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index cc5b398d..2e649565 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -613,8 +613,10 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea if (context.MetadataContext.PdbLoaded) { // **Note** Interface types will not be found + // as they have no executable code locations + // Workround -- add a marker inner type - // Allow for F# modules that contain only types + // Allow for e.g. F# modules that contain only types var documents = (new[] { typeInfo.FullName }).Concat( typeInfo.DeclaredNestedTypes.Select(t => t.FullName)) .SelectMany(n => context.MetadataContext[n]) From e3e89375b9522bfbf8323df51230ec8778eb52ee Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Sun, 7 Jun 2020 13:58:22 +0100 Subject: [PATCH 05/10] Fix metadata reader options --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index 2e649565..885b508a 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -2276,7 +2276,7 @@ public MetadataContext(string assemblyPath) { Func streamProvider = p => new FileStream(p, FileMode.Open, FileAccess.Read); - var metadata = reader.GetMetadataReader(MetadataReaderOptions.ApplyWindowsRuntimeProjections); + var metadata = reader.GetMetadataReader(MetadataReaderOptions.Default); PdbLoaded = reader.TryOpenAssociatedPortablePdb(stream.Name, streamProvider, out var metadataReaderProvider, out var pdbPath); From a486f604708790a87d23450be315ab47dcccc299 Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Tue, 9 Jun 2020 11:37:48 +0100 Subject: [PATCH 06/10] Expand comment --- src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index 885b508a..3e103ab5 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -612,9 +612,17 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea { if (context.MetadataContext.PdbLoaded) { - // **Note** Interface types will not be found - // as they have no executable code locations + // **Note** Types that have no executable code locations + // e.g. Interfaces, enums, derived types with only + // default constructor and inherited methods, + // will not be found. // Workround -- add a marker inner type + // Heavier weight workround -- provide an SDK with + // an attribute that is constructed with a + // [System.Runtime.CompilerServices.CallerFilePath] + // argument and reflect for its value + // There's nowhere in the .pdb format to add an + // arbitrary type -> file mapping // Allow for e.g. F# modules that contain only types var documents = (new[] { typeInfo.FullName }).Concat( From bb08f1190dfeb77fd72e412ec41d5c72c09440eb Mon Sep 17 00:00:00 2001 From: Steve Gilham Date: Tue, 9 Jun 2020 14:16:24 +0100 Subject: [PATCH 07/10] Defer the choice to the user --- docs/XmlDocMarkdown.Core.md | 1 + .../XmlDocMarkdownSettings.md | 1 + .../XmlDocMarkdownSettings/SourceCodeStyle.md | 19 +++++++++++ .../XmlDocSourceCodeStyle.md | 24 +++++++++++++ src/XmlDocMarkdown.Core/MarkdownGenerator.cs | 28 ++++++++++++--- .../XmlDocMarkdownGenerator.cs | 1 + .../XmlDocMarkdownSettings.cs | 7 ++++ .../XmlDocSourceCodeStyle.cs | 34 +++++++++++++++++++ src/xmldocmd/CommonArgs.cs | 15 ++++++++ src/xmldocmd/XmlDocMarkdownApp.cs | 6 ++++ 10 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings/SourceCodeStyle.md create mode 100644 docs/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.md create mode 100644 src/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.cs diff --git a/docs/XmlDocMarkdown.Core.md b/docs/XmlDocMarkdown.Core.md index ffb01468..cfc798e8 100644 --- a/docs/XmlDocMarkdown.Core.md +++ b/docs/XmlDocMarkdown.Core.md @@ -8,6 +8,7 @@ | static class [XmlDocMarkdownGenerator](XmlDocMarkdown.Core/XmlDocMarkdownGenerator.md) | Generates Markdown from .NET XML documentation comments. | | class [XmlDocMarkdownResult](XmlDocMarkdown.Core/XmlDocMarkdownResult.md) | The names of files that were added, changed, or removed. | | class [XmlDocMarkdownSettings](XmlDocMarkdown.Core/XmlDocMarkdownSettings.md) | Settings for markdown generation. | +| [Flags] enum [XmlDocSourceCodeStyle](XmlDocMarkdown.Core/XmlDocSourceCodeStyle.md) | The approach used for documenting source code paths, if `--source` is given. | | enum [XmlDocVisibilityLevel](XmlDocMarkdown.Core/XmlDocVisibilityLevel.md) | The minimum visibility for documented types and members. | diff --git a/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings.md b/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings.md index f73e4402..172eea42 100644 --- a/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings.md +++ b/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings.md @@ -24,6 +24,7 @@ public class XmlDocMarkdownSettings | [ShouldClean](XmlDocMarkdownSettings/ShouldClean.md) { get; set; } | If true, deletes previously generated files that are no longer used. | | [SkipUnbrowsable](XmlDocMarkdownSettings/SkipUnbrowsable.md) { get; set; } | If true, skips documentation for types and members with `[EditorBrowsable(EditorBrowsableState.Never)]`. | | [SourceCodePath](XmlDocMarkdownSettings/SourceCodePath.md) { get; set; } | The URL of the folder containing the source code of the assembly, e.g. at GitHub. | +| [SourceCodeStyle](XmlDocMarkdownSettings/SourceCodeStyle.md) { get; set; } | The manner in which the URL path is derived, | | [TocPrefix](XmlDocMarkdownSettings/TocPrefix.md) { get; set; } | A path prefix to add to all links in the table of contents .yml file. | | [VisibilityLevel](XmlDocMarkdownSettings/VisibilityLevel.md) { get; set; } | The minimum visibility for documented types and members. | diff --git a/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings/SourceCodeStyle.md b/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings/SourceCodeStyle.md new file mode 100644 index 00000000..9fd17ac4 --- /dev/null +++ b/docs/XmlDocMarkdown.Core/XmlDocMarkdownSettings/SourceCodeStyle.md @@ -0,0 +1,19 @@ +# XmlDocMarkdownSettings.SourceCodeStyle property + +The manner in which the URL path is derived, + +```csharp +public XmlDocSourceCodeStyle? SourceCodeStyle { get; set; } +``` + +## Remarks + +May be a combination of flags, with the preference order being SourceLink, DebugSymbol and finally TypeName + +## See Also + +* enum [XmlDocSourceCodeStyle](../XmlDocSourceCodeStyle.md) +* class [XmlDocMarkdownSettings](../XmlDocMarkdownSettings.md) +* namespace [XmlDocMarkdown.Core](../../XmlDocMarkdown.Core.md) + + diff --git a/docs/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.md b/docs/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.md new file mode 100644 index 00000000..334f5b0c --- /dev/null +++ b/docs/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.md @@ -0,0 +1,24 @@ +# XmlDocSourceCodeStyle enumeration + +The approach used for documenting source code paths, if `--source` is given. + +```csharp +[Flags] +public enum XmlDocSourceCodeStyle +``` + +## Values + +| name | value | description | +| --- | --- | --- | +| Default | `0x0` | Path is based on type name, and C# is assumed. | +| TypeName | `0x1` | If no other path is found, report based on type name, and C# is assumed. | +| DebugSymbol | `0x2` | If debug symbol information is found, report based on the document(s) in which the type has members. | +| SourceLink | `0x4` | If debug symbol information is found, and sourcelink information is present, use that to report based on the document(s) in which the type has members, in preference to all else. | + +## See Also + +* namespace [XmlDocMarkdown.Core](../XmlDocMarkdown.Core.md) +* [XmlDocSourceCodeStyle.cs](../../src/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.cs) + + diff --git a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs index 3e103ab5..3475a812 100644 --- a/src/XmlDocMarkdown.Core/MarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/MarkdownGenerator.cs @@ -23,6 +23,8 @@ internal sealed class MarkdownGenerator public string SourceCodePath { get; set; } + public XmlDocSourceCodeStyle SourceCodeStyle { get; set; } + public string RootNamespace { get; set; } public string RootPageLocation { get; set; } @@ -100,7 +102,8 @@ private IEnumerable DoGenerateOutput(Assembly assembly, XmlDocAssembl string rootNamespace = RootNamespace ?? visibleNamespaceRecords.OrderBy(x => x.Namespace.Length).ThenByDescending(x => x.Types.Count).Select(x => x.Namespace).FirstOrDefault(x => x.Length != 0); RootPageLocation = $"{safeAssemblyName}" + (PermalinkPretty ? "Assembly.md" : ".md"); - var context = new MarkdownContext(xmlDocAssembly, membersByXmlDocName, assemblyFileName, sourceCodePath, rootNamespace, RootPageLocation, assembly.Location); + var context = new MarkdownContext(xmlDocAssembly, membersByXmlDocName, assemblyFileName, + sourceCodePath, SourceCodeStyle, rootNamespace, RootPageLocation, assembly.Location); yield return CreateNamedText(context.PageLocation, null, assemblyName, writer => { var front = GetFrontMatter(assemblyName, $"{safeAssemblyName}" + (PermalinkPretty ? "Assembly" : "") + extension); @@ -610,7 +613,11 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea if (typeInfo != null && declaringType == null && !string.IsNullOrEmpty(context.SourceCodePath) && !string.IsNullOrEmpty(context.RootNamespace)) { - if (context.MetadataContext.PdbLoaded) + var written = false; + var mask = XmlDocSourceCodeStyle.SourceLink | XmlDocSourceCodeStyle.DebugSymbol; + + if (context.MetadataContext.PdbLoaded && + (context.SourceCodeStyle & mask) != 0) { // **Note** Types that have no executable code locations // e.g. Interfaces, enums, derived types with only @@ -633,7 +640,8 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea foreach (var document in documents) { var fileName = Path.GetFileName(document); - if (context.MetadataContext.TrySourceLink(document, out var link)) + if ((context.SourceCodeStyle & XmlDocSourceCodeStyle.SourceLink) != 0 + && context.MetadataContext.TrySourceLink(document, out var link)) { writer.WriteLine($"* [{fileName}]({link})"); } @@ -645,9 +653,13 @@ private NamedText WriteMemberPage(string path, string parent, string title, IRea filePath = "../" + filePath; writer.WriteLine($"* [{fileName}]({filePath})"); } + written = true; } } - else // default to old behaviour if .PDB cannot be read + + // default to old behaviour if requested + if (!written && + (context.SourceCodeStyle & XmlDocSourceCodeStyle.TypeName) != 0) { string namespaceName = GetNamespaceName(typeInfo); if (namespaceName.StartsWith(context.RootNamespace, StringComparison.Ordinal)) @@ -2207,12 +2219,15 @@ private IEnumerable ToMarkdown(IReadOnlyList blocks, Markdo private class MarkdownContext { - public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary membersByXmlDocName, string assemblyFileName, string sourceCodePath, string rootNamespace, string pageLocation, string assemblyLocation) + public MarkdownContext(XmlDocAssembly xmlDocAssembly, IReadOnlyDictionary membersByXmlDocName, + string assemblyFileName, string sourceCodePath, XmlDocSourceCodeStyle sourceCodeStyle, string rootNamespace, + string pageLocation, string assemblyLocation) { XmlDocAssembly = xmlDocAssembly; MembersByXmlDocName = membersByXmlDocName; AssemblyFileName = assemblyFileName; SourceCodePath = sourceCodePath; + SourceCodeStyle = sourceCodeStyle; RootNamespace = rootNamespace; PageLocation = pageLocation; @@ -2227,6 +2242,7 @@ public MarkdownContext(MarkdownContext context, MemberInfo memberInfo, string pa MembersByXmlDocName = context.MembersByXmlDocName; AssemblyFileName = context.AssemblyFileName; SourceCodePath = context.SourceCodePath; + SourceCodeStyle = context.SourceCodeStyle; RootNamespace = context.RootNamespace; PageLocation = pageLocation; MetadataContext = context.MetadataContext; @@ -2255,6 +2271,8 @@ public MarkdownContext(MarkdownContext context, MemberInfo memberInfo, string pa public string SourceCodePath { get; } + public XmlDocSourceCodeStyle SourceCodeStyle { get; } + public string RootNamespace { get; } public string PageLocation { get; } diff --git a/src/XmlDocMarkdown.Core/XmlDocMarkdownGenerator.cs b/src/XmlDocMarkdown.Core/XmlDocMarkdownGenerator.cs index cf6a119b..634b6f8e 100644 --- a/src/XmlDocMarkdown.Core/XmlDocMarkdownGenerator.cs +++ b/src/XmlDocMarkdown.Core/XmlDocMarkdownGenerator.cs @@ -34,6 +34,7 @@ public static XmlDocMarkdownResult Generate(string inputPath, string outputPath, var generator = new MarkdownGenerator { SourceCodePath = settings.SourceCodePath, + SourceCodeStyle = settings.SourceCodeStyle ?? XmlDocSourceCodeStyle.TypeName, RootNamespace = settings.RootNamespace, IncludeObsolete = settings.IncludeObsolete, SkipUnbrowsable = settings.SkipUnbrowsable, diff --git a/src/XmlDocMarkdown.Core/XmlDocMarkdownSettings.cs b/src/XmlDocMarkdown.Core/XmlDocMarkdownSettings.cs index 994ab2de..8d63e0cb 100644 --- a/src/XmlDocMarkdown.Core/XmlDocMarkdownSettings.cs +++ b/src/XmlDocMarkdown.Core/XmlDocMarkdownSettings.cs @@ -14,6 +14,13 @@ public class XmlDocMarkdownSettings /// links in the See Also sections for types. public string SourceCodePath { get; set; } + /// + /// The manner in which the URL path is derived, + /// + /// May be a combination of flags, with the preference order being + /// SourceLink, DebugSymbol and finally TypeName + public XmlDocSourceCodeStyle? SourceCodeStyle { get; set; } + /// /// The root namespace of the input assembly. /// diff --git a/src/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.cs b/src/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.cs new file mode 100644 index 00000000..d631cad2 --- /dev/null +++ b/src/XmlDocMarkdown.Core/XmlDocSourceCodeStyle.cs @@ -0,0 +1,34 @@ +using System; + +namespace XmlDocMarkdown.Core +{ + /// + /// The approach used for documenting source code paths, if `--source` is given. + /// + [Flags] + public enum XmlDocSourceCodeStyle + { + /// + /// Path is based on type name, and C# is assumed. + /// + Default = 0, + + /// + /// If no other path is found, report based on type name, and C# is assumed. + /// + TypeName = 1, + + /// + /// If debug symbol information is found, report based on the document(s) + /// in which the type has members. + /// + DebugSymbol = 2, + + /// + /// If debug symbol information is found, and sourcelink information is + /// present, use that to report based on the document(s) + /// in which the type has members, in preference to all else. + /// + SourceLink = 4 + } +} diff --git a/src/xmldocmd/CommonArgs.cs b/src/xmldocmd/CommonArgs.cs index b986da7f..50e4b134 100644 --- a/src/xmldocmd/CommonArgs.cs +++ b/src/xmldocmd/CommonArgs.cs @@ -1,3 +1,4 @@ +using System; using XmlDocMarkdown.Core; namespace XmlDocMarkdown @@ -9,6 +10,20 @@ public static string ReadSourceOption(this ArgsReader args) return args.ReadOption("source"); } + public static XmlDocSourceCodeStyle? ReadSourceStyleOption(this ArgsReader args) + { + string style = args.ReadOption("source-style"); + if (style == null) + return null; + + if (!Enum.TryParse(style, true, out var choice)) + throw new ArgsReaderException($"Unknown source code style option: {style}"); + + return choice == XmlDocSourceCodeStyle.Default ? + XmlDocSourceCodeStyle.TypeName : + choice; + } + public static string ReadNamespaceOption(this ArgsReader args) { return args.ReadOption("namespace"); diff --git a/src/xmldocmd/XmlDocMarkdownApp.cs b/src/xmldocmd/XmlDocMarkdownApp.cs index 6c8e4c34..4c31bfa6 100644 --- a/src/xmldocmd/XmlDocMarkdownApp.cs +++ b/src/xmldocmd/XmlDocMarkdownApp.cs @@ -28,6 +28,7 @@ public int Run(IReadOnlyList args) var settings = new XmlDocMarkdownSettings(); settings.NewLine = argsReader.ReadNewLineOption(); settings.SourceCodePath = argsReader.ReadSourceOption(); + settings.SourceCodeStyle = argsReader.ReadSourceStyleOption(); settings.RootNamespace = argsReader.ReadNamespaceOption(); settings.IncludeObsolete = argsReader.ReadObsoleteFlag(); settings.SkipUnbrowsable = argsReader.ReadSkipUnbrowsableFlag(); @@ -101,6 +102,11 @@ private void WriteUsage(TextWriter textWriter) textWriter.WriteLine(" --source "); textWriter.WriteLine(" The URL (absolute or relative) of the folder containing the source"); textWriter.WriteLine(" code of the assembly, e.g. at GitHub. (optional)"); + textWriter.WriteLine(" --source-style