diff --git a/.gitignore b/.gitignore
index 968a077..29cdd6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ bin
obj
.ionide/
artifacts/
-*/*.user
\ No newline at end of file
+*/*.user
+*.received.*
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..c411604
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/DocsPortingTool.sln b/DocsPortingTool.sln
index b39f52d..bd0be87 100644
--- a/DocsPortingTool.sln
+++ b/DocsPortingTool.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.28705.295
+# Visual Studio Version 17
+VisualStudioVersion = 17.1.31926.61
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libraries", "Libraries\Libraries.csproj", "{87BBF4FD-260C-4AC4-802B-7D2B29629C07}"
EndProject
@@ -11,9 +11,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
BackportInstructions.md = BackportInstructions.md
+ Directory.Build.targets = Directory.Build.targets
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
global.json = global.json
install-as-tool.ps1 = install-as-tool.ps1
+ Packages.props = Packages.props
README.md = README.md
EndProjectSection
EndProject
diff --git a/Libraries/Docs/APIKind.cs b/Libraries/Docs/APIKind.cs
index 00f554e..5781f84 100644
--- a/Libraries/Docs/APIKind.cs
+++ b/Libraries/Docs/APIKind.cs
@@ -1,6 +1,6 @@
namespace Libraries.Docs
{
- internal enum APIKind
+ public enum APIKind
{
Type,
Member
diff --git a/Libraries/Docs/DocsAPI.cs b/Libraries/Docs/DocsAPI.cs
index 6ba7b8f..c78169d 100644
--- a/Libraries/Docs/DocsAPI.cs
+++ b/Libraries/Docs/DocsAPI.cs
@@ -6,8 +6,11 @@
namespace Libraries.Docs
{
- internal abstract class DocsAPI : IDocsAPI
+ public abstract class DocsAPI : IDocsAPI
{
+ private DocsSummary? _summary;
+ private DocsRemarks? _remarks;
+ private List? _examples;
private List? _params;
private List? _parameters;
private List? _typeParameters;
@@ -156,7 +159,7 @@ public List AltMembers
{
if (Docs != null)
{
- _altMemberCrefs = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref").DocIdEscaped()).ToList();
+ _altMemberCrefs = Docs.Elements("altmember").Select(x => XmlHelper.GetAttributeValue(x, "cref")).ToList();
}
else
{
@@ -190,8 +193,71 @@ public List Relateds
public abstract string ReturnType { get; }
public abstract string Returns { get; set; }
+ public DocsSummary SummaryElement
+ {
+ get
+ {
+ if (_summary == null)
+ {
+ XElement? xe = Docs?.Element("summary");
+
+ if (xe != null)
+ {
+ _summary = new(xe);
+ }
+ else
+ {
+ throw new InvalidOperationException($"There was no element. Doc ID: {DocId}");
+ }
+ }
+
+ return _summary;
+ }
+ }
+
public abstract string Remarks { get; set; }
+ public DocsRemarks RemarksElement
+ {
+ get
+ {
+ if (_remarks == null)
+ {
+ XElement? xe = Docs?.Element("remarks");
+
+ if (xe != null)
+ {
+ _remarks = new(xe);
+
+ if (!_remarks.ExampleContent?.ParsedText?.IsDocsEmpty() ?? false)
+ {
+ ExampleElements.Add(_remarks.ExampleContent!);
+ }
+ }
+ else
+ {
+ _remarks = new(new XElement("remarks"));
+ }
+ }
+
+ return _remarks;
+ }
+ }
+
+ public List ExampleElements
+ {
+ get
+ {
+ if (_examples == null)
+ {
+ IEnumerable elems = Docs.Elements("example");
+ _examples = elems.Select(e => new DocsExample(e)).ToList();
+ }
+
+ return _examples;
+ }
+ }
+
public List AssemblyInfos
{
get
@@ -206,10 +272,10 @@ public List AssemblyInfos
public DocsParam SaveParam(XElement xeIntelliSenseXmlParam)
{
- XElement xeDocsParam = new XElement(xeIntelliSenseXmlParam.Name);
+ XElement xeDocsParam = new(xeIntelliSenseXmlParam.Name);
xeDocsParam.ReplaceAttributes(xeIntelliSenseXmlParam.Attributes());
XmlHelper.SaveFormattedAsXml(xeDocsParam, xeIntelliSenseXmlParam.Value);
- DocsParam docsParam = new DocsParam(this, xeDocsParam);
+ DocsParam docsParam = new(this, xeDocsParam);
Changed = true;
return docsParam;
}
@@ -229,7 +295,7 @@ public APIKind Kind
public DocsTypeParam AddTypeParam(string name, string value)
{
- XElement typeParam = new XElement("typeparam");
+ XElement typeParam = new("typeparam");
typeParam.SetAttributeValue("name", name);
XmlHelper.AddChildFormattedAsXml(Docs, typeParam, value);
Changed = true;
diff --git a/Libraries/Docs/DocsApiReference.cs b/Libraries/Docs/DocsApiReference.cs
new file mode 100644
index 0000000..733916c
--- /dev/null
+++ b/Libraries/Docs/DocsApiReference.cs
@@ -0,0 +1,129 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+using static System.Net.WebUtility;
+
+namespace Libraries.Docs
+{
+ public class DocsApiReference
+ {
+ public bool IsOverload { get; private init; }
+
+ public char? Prefix { get; private init; }
+
+ public string Api { get; private init; }
+
+ // Generic parameters need to support both single and double backtick conventions
+ private const string GenericParameterPattern = @"`{1,2}(?\d+)";
+ private const string ApiChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;`%]";
+ private const string ApiReferencePattern = @"((?[A-Za-z]):)?(?(" + ApiChars + @")+)?(?\?(" + ApiChars + @")+=(" + ApiChars + @")+)?";
+ private static readonly Regex XrefPattern = new("" + ApiReferencePattern + ")\\s*>", RegexOptions.Compiled);
+
+ public DocsApiReference(string apiReference)
+ {
+ Api = UrlDecode(apiReference);
+ var match = Regex.Match(Api, ApiReferencePattern);
+
+ if (match.Success)
+ {
+ Api = match.Groups["api"].Value;
+
+ if (match.Groups["prefix"].Success)
+ {
+ Prefix = match.Groups["prefix"].Value[0];
+ IsOverload = Prefix == 'O';
+ }
+ }
+
+ if (Api.EndsWith('*'))
+ {
+ IsOverload = true;
+ Api = Api[..^1];
+ }
+
+ Api = ReplacePrimitivesWithShorthands(Api);
+ Api = ParseGenericTypes(Api);
+ }
+
+ public override string ToString()
+ {
+ if (Prefix is not null)
+ {
+ return $"{Prefix}:{Api}";
+ }
+
+ return Api;
+ }
+
+ private static readonly Dictionary PrimitiveTypes = new()
+ {
+ { "System.Boolean", "bool" },
+ { "System.Byte", "byte" },
+ { "System.Char", "char" },
+ { "System.Decimal", "decimal" },
+ { "System.Double", "double" },
+ { "System.Int16", "short" },
+ { "System.Int32", "int" },
+ { "System.Int64", "long" },
+ { "System.Object", "object" }, // Ambiguous: could be 'object' or 'dynamic' https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types
+ { "System.SByte", "sbyte" },
+ { "System.Single", "float" },
+ { "System.String", "string" },
+ { "System.UInt16", "ushort" },
+ { "System.UInt32", "uint" },
+ { "System.UInt64", "ulong" },
+ { "System.Void", "void" }
+ };
+
+ public static string ReplacePrimitivesWithShorthands(string apiReference)
+ {
+ foreach ((string key, string value) in PrimitiveTypes)
+ {
+ apiReference = Regex.Replace(apiReference, key, value);
+ }
+
+ return apiReference;
+ }
+
+ public static string ParseGenericTypes(string apiReference)
+ {
+ int genericParameterArity = 0;
+ return Regex.Replace(apiReference, GenericParameterPattern, MapGenericParameter);
+
+ string MapGenericParameter(Match match)
+ {
+ int arity = int.Parse(match.Groups["arity"].Value);
+
+ if (genericParameterArity == 0)
+ {
+ // This is the first match that declares the generic parameter arity of the method
+ // e.g. GenericMethod``3 ---> GenericMethod{T1,T2,T3}(...);
+ Debug.Assert(arity > 0);
+ genericParameterArity = arity;
+ return WrapInCurlyBrackets(string.Join(",", Enumerable.Range(0, arity).Select(CreateGenericParameterName)));
+ }
+
+ // Subsequent matches are references to generic parameters in the method signature,
+ // e.g. GenericMethod{T1,T2,T3}(..., List{``1} parameter, ...); ---> List{T2} parameter
+ return CreateGenericParameterName(arity);
+
+ // This naming scheme does not map to the exact generic parameter names;
+ // however this is still accepted by intellisense and backporters can rename
+ // manually with the help of tooling.
+ string CreateGenericParameterName(int index) => genericParameterArity == 1 ? "T" : $"T{index + 1}";
+
+ static string WrapInCurlyBrackets(string input) => $"{{{input}}}";
+ }
+ }
+
+ public static string ReplaceMarkdownXrefWithSeeCref(string markdown)
+ {
+ return XrefPattern.Replace(markdown, match =>
+ {
+ var api = new DocsApiReference(match.Groups["api"].Value);
+ return @$"";
+ });
+ }
+ }
+}
diff --git a/Libraries/Docs/DocsAssemblyInfo.cs b/Libraries/Docs/DocsAssemblyInfo.cs
index 7840315..6b0c0af 100644
--- a/Libraries/Docs/DocsAssemblyInfo.cs
+++ b/Libraries/Docs/DocsAssemblyInfo.cs
@@ -4,7 +4,7 @@
namespace Libraries.Docs
{
- internal class DocsAssemblyInfo
+ public class DocsAssemblyInfo
{
private readonly XElement XEAssemblyInfo;
public string AssemblyName
diff --git a/Libraries/Docs/DocsAttribute.cs b/Libraries/Docs/DocsAttribute.cs
index a07ad42..2c83588 100644
--- a/Libraries/Docs/DocsAttribute.cs
+++ b/Libraries/Docs/DocsAttribute.cs
@@ -1,8 +1,9 @@
-using System.Xml.Linq;
+using System.Linq;
+using System.Xml.Linq;
namespace Libraries.Docs
{
- internal class DocsAttribute
+ public class DocsAttribute
{
private readonly XElement XEAttribute;
@@ -13,12 +14,15 @@ public string FrameworkAlternate
return XmlHelper.GetAttributeValue(XEAttribute, "FrameworkAlternate");
}
}
- public string AttributeName
+
+ public string? AttributeName
{
- get
- {
- return XmlHelper.GetChildElementValue(XEAttribute, "AttributeName");
- }
+ get => GetAttributeName("C#");
+ }
+
+ public string? GetAttributeName(string language)
+ {
+ return XEAttribute.Elements("AttributeName").Where(x => XmlHelper.GetAttributeValue(x, "Language") == language).SingleOrDefault()?.Value;
}
public DocsAttribute(XElement xeAttribute)
diff --git a/Libraries/Docs/DocsCommentsContainer.cs b/Libraries/Docs/DocsCommentsContainer.cs
index a8ab30a..880e7ae 100644
--- a/Libraries/Docs/DocsCommentsContainer.cs
+++ b/Libraries/Docs/DocsCommentsContainer.cs
@@ -8,14 +8,14 @@
namespace Libraries.Docs
{
- internal class DocsCommentsContainer
+ public class DocsCommentsContainer
{
private Configuration Config { get; set; }
private XDocument? xDoc = null;
- public readonly Dictionary Types = new();
- public readonly Dictionary Members = new();
+ internal readonly Dictionary Types = new();
+ internal readonly Dictionary Members = new();
public DocsCommentsContainer(Configuration config)
{
diff --git a/Libraries/Docs/DocsExample.cs b/Libraries/Docs/DocsExample.cs
new file mode 100644
index 0000000..ebe0a01
--- /dev/null
+++ b/Libraries/Docs/DocsExample.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace Libraries.Docs
+{
+ public class DocsExample : DocsMarkdownElement
+ {
+ public DocsExample(XElement xeExample) : base(xeExample)
+ {
+ }
+
+ protected override string ExtractElements(string markdown)
+ {
+ markdown = base.ExtractElements(markdown);
+ markdown = RemoveMarkdownHeading(markdown, "Examples?");
+
+ return markdown;
+ }
+ }
+}
diff --git a/Libraries/Docs/DocsException.cs b/Libraries/Docs/DocsException.cs
index d5b26cd..0266840 100644
--- a/Libraries/Docs/DocsException.cs
+++ b/Libraries/Docs/DocsException.cs
@@ -5,7 +5,7 @@
namespace Libraries.Docs
{
- internal class DocsException
+ public class DocsException
{
private readonly XElement XEException;
diff --git a/Libraries/Docs/DocsMarkdownElement.cs b/Libraries/Docs/DocsMarkdownElement.cs
new file mode 100644
index 0000000..b971dc6
--- /dev/null
+++ b/Libraries/Docs/DocsMarkdownElement.cs
@@ -0,0 +1,141 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace Libraries.Docs
+{
+ public abstract class DocsMarkdownElement : DocsTextElement
+ {
+ public DocsMarkdownElement(XElement xeRemarks) : base(xeRemarks)
+ {
+ }
+
+ public IEnumerable? Params { get; init; }
+ public IEnumerable? TypeParams { get; init; }
+
+ private static readonly Regex IncludeFilePattern = new(@"\[!INCLUDE", RegexOptions.Compiled);
+ private static readonly Regex CalloutPattern = new(@"\[!NOTE|\[!IMPORTANT|\[!TIP", RegexOptions.Compiled);
+ private static readonly Regex CodeIncludePattern = new(@"\[!code-cpp|\[!code-csharp|\[!code-vb", RegexOptions.Compiled);
+
+ private static readonly Regex MarkdownLinkPattern = new(@"\[(?.+)\]\((?(http|www)([A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;\?=%])+)\)", RegexOptions.Compiled);
+ private const string MarkdownLinkReplacement = "${linkText}";
+
+ private static readonly Regex MarkdownBoldPattern = new(@"\*\*(?[A-Za-z0-9\-\._~:\/#\[\]@!\$&'\(\)\+,;%` ]+)\*\*", RegexOptions.Compiled);
+ private const string MarkdownBoldReplacement = @"${content}";
+
+ private static readonly Regex MarkdownCodeStartPattern = new(@"```(?(cs|csharp|cpp|vb|visualbasic))(?\s+)", RegexOptions.Compiled);
+ private const string MarkdownCodeStartReplacement = "${spaces}";
+
+ private static readonly Regex MarkdownCodeEndPattern = new(@"```(?\s+)", RegexOptions.Compiled);
+ private const string MarkdownCodeEndReplacement = "
${spaces}";
+
+ private static readonly Regex UnparseableMarkdown = new(string.Join('|', new[] {
+ IncludeFilePattern.ToString(),
+ CalloutPattern.ToString(),
+ CodeIncludePattern.ToString()
+ }), RegexOptions.Compiled);
+
+ protected override string? ParseNode(XNode node)
+ {
+ if (node is XElement element && element.Name == "format" && element.Attribute("type")?.Value == "text/markdown")
+ {
+ var cdata = element.FirstNode as XCData;
+ var markdown = cdata?.Value ?? element.Value;
+
+ markdown = ExtractElements(markdown);
+
+ // If we're able to fully parse the markdown, then
+ // we can just return the parsed text
+ if (TryParseMarkdown(markdown, out var parsedText))
+ {
+ return parsedText;
+ }
+ else
+ {
+ // But if the markdown couldn't be fully parsed,
+ // then update the element with the markdown
+ // that remains after extracting other elements,
+ // retaining the CDATA wrapper if it was present
+ if (cdata is not null)
+ {
+ cdata.Value = markdown;
+ }
+ else
+ {
+ element.Value = markdown;
+ }
+ }
+ }
+
+ return base.ParseNode(node);
+ }
+
+ protected string RemoveMarkdownHeading(string markdown, string heading)
+ {
+ Regex HeadingPattern = new(@$"^\s*##\s*{heading}\s*$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
+ return HeadingPattern.Replace(markdown, "", 1);
+ }
+
+ protected virtual string ExtractElements(string markdown) => markdown;
+
+ protected virtual bool TryParseMarkdown(string markdown, [NotNullWhen(true)] out string? parsed)
+ {
+ if (UnparseableMarkdown.IsMatch(markdown))
+ {
+ parsed = null;
+ return false;
+ }
+
+ parsed = DocsApiReference.ReplaceMarkdownXrefWithSeeCref(markdown);
+ parsed = MarkdownLinkPattern.Replace(parsed, MarkdownLinkReplacement);
+ parsed = MarkdownBoldPattern.Replace(parsed, MarkdownBoldReplacement);
+ parsed = MarkdownCodeStartPattern.Replace(parsed, MarkdownCodeStartReplacement);
+ parsed = MarkdownCodeEndPattern.Replace(parsed, MarkdownCodeEndReplacement);
+ parsed = ReplaceBacktickReferences(parsed);
+
+ return true;
+ }
+
+ private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" };
+
+ private string ReplaceBacktickReferences(string markdown)
+ {
+ // langwords|parameters|typeparams and other type references within markdown backticks
+ MatchCollection collection = Regex.Matches(markdown, @"(?`(?[a-zA-Z0-9_\.]+(?\<(?[a-zA-Z0-9_,]+)\>){0,1})`)");
+ foreach (Match match in collection)
+ {
+ string backtickContent = match.Groups["backtickContent"].Value;
+ string backtickedApi = match.Groups["backtickedApi"].Value;
+ Group genericType = match.Groups["genericType"];
+ Group typeParam = match.Groups["typeParam"];
+
+ if (genericType.Success && typeParam.Success)
+ {
+ backtickedApi = backtickedApi.Replace(genericType.Value, $"{{{typeParam.Value}}}");
+ }
+
+ if (ReservedKeywords.Any(x => x == backtickedApi))
+ {
+ markdown = Regex.Replace(markdown, $"{backtickContent}", $"");
+ }
+ else if (TypeParams?.Any(x => x.Name == backtickedApi) == true)
+ {
+ markdown = Regex.Replace(markdown, $"{backtickContent}", $"");
+ }
+ else if (Params?.Any(x => x.Name == backtickedApi) == true)
+ {
+ markdown = Regex.Replace(markdown, $"{backtickContent}", $"");
+ }
+ else
+ {
+ markdown = Regex.Replace(markdown, $"{backtickContent}", $"");
+ }
+ }
+
+ return markdown;
+ }
+ }
+}
+
diff --git a/Libraries/Docs/DocsMember.cs b/Libraries/Docs/DocsMember.cs
index c7a5304..19ccf52 100644
--- a/Libraries/Docs/DocsMember.cs
+++ b/Libraries/Docs/DocsMember.cs
@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
namespace Libraries.Docs
{
- internal class DocsMember : DocsAPI
+ public class DocsMember : DocsAPI
{
private string? _memberName;
private List? _memberSignatures;
@@ -65,7 +66,9 @@ public override string DocId
string message = string.Format("Could not find a DocId MemberSignature for '{0}'", MemberName);
throw new Exception(message);
}
- _docId = ms.Value.DocIdEscaped();
+
+ // This value must not be unescaped
+ _docId = ms.Value;
}
return _docId;
}
diff --git a/Libraries/Docs/DocsMemberSignature.cs b/Libraries/Docs/DocsMemberSignature.cs
index f9eff57..0a2dc46 100644
--- a/Libraries/Docs/DocsMemberSignature.cs
+++ b/Libraries/Docs/DocsMemberSignature.cs
@@ -2,7 +2,7 @@
namespace Libraries.Docs
{
- internal class DocsMemberSignature
+ public class DocsMemberSignature
{
private readonly XElement XEMemberSignature;
diff --git a/Libraries/Docs/DocsParam.cs b/Libraries/Docs/DocsParam.cs
index c5a09b2..6a1591e 100644
--- a/Libraries/Docs/DocsParam.cs
+++ b/Libraries/Docs/DocsParam.cs
@@ -2,7 +2,7 @@
namespace Libraries.Docs
{
- internal class DocsParam
+ public class DocsParam : DocsTextElement
{
private readonly XElement XEDocsParam;
public IDocsAPI ParentAPI
@@ -28,7 +28,7 @@ public string Value
ParentAPI.Changed = true;
}
}
- public DocsParam(IDocsAPI parentAPI, XElement xeDocsParam)
+ public DocsParam(IDocsAPI parentAPI, XElement xeDocsParam) : base(xeDocsParam)
{
ParentAPI = parentAPI;
XEDocsParam = xeDocsParam;
diff --git a/Libraries/Docs/DocsParameter.cs b/Libraries/Docs/DocsParameter.cs
index ec598b8..a4a090b 100644
--- a/Libraries/Docs/DocsParameter.cs
+++ b/Libraries/Docs/DocsParameter.cs
@@ -2,7 +2,7 @@
namespace Libraries.Docs
{
- internal class DocsParameter
+ public class DocsParameter : DocsTextElement
{
private readonly XElement XEParameter;
public string Name
@@ -19,7 +19,7 @@ public string Type
return XmlHelper.GetAttributeValue(XEParameter, "Type");
}
}
- public DocsParameter(XElement xeParameter)
+ public DocsParameter(XElement xeParameter) : base(xeParameter)
{
XEParameter = xeParameter;
}
diff --git a/Libraries/Docs/DocsRelated.cs b/Libraries/Docs/DocsRelated.cs
index 360b1f6..fabf748 100644
--- a/Libraries/Docs/DocsRelated.cs
+++ b/Libraries/Docs/DocsRelated.cs
@@ -2,7 +2,7 @@
namespace Libraries.Docs
{
- internal class DocsRelated
+ public class DocsRelated
{
private readonly XElement XERelatedArticle;
diff --git a/Libraries/Docs/DocsRemarks.cs b/Libraries/Docs/DocsRemarks.cs
new file mode 100644
index 0000000..d1a85f8
--- /dev/null
+++ b/Libraries/Docs/DocsRemarks.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+
+namespace Libraries.Docs
+{
+ public class DocsRemarks : DocsMarkdownElement
+ {
+ public DocsRemarks(XElement xeRemarks) : base(xeRemarks)
+ {
+ }
+
+ private DocsExample? _exampleContent;
+
+ public DocsExample? ExampleContent
+ {
+ get
+ {
+ EnsureParsed();
+ return _exampleContent;
+ }
+ set
+ {
+ _exampleContent = value;
+ }
+ }
+
+ private static readonly Regex ExampleSectionPattern = new(@"^\s*##\s*Examples?\s*(?.*)", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
+
+ protected override string ExtractElements(string markdown)
+ {
+ markdown = base.ExtractElements(markdown);
+ markdown = RemoveMarkdownHeading(markdown, "Remarks");
+ markdown = ExtractExamples(markdown);
+
+ return markdown;
+ }
+
+ private string ExtractExamples(string markdown)
+ {
+ var match = ExampleSectionPattern.Match(markdown);
+
+ if (match.Success)
+ {
+ string exampleContent = match.Groups["examples"].Value;
+ string exampleXml = $@"";
+
+ // Extract the examples (as a side effect)
+ ExampleContent = new DocsExample(XElement.Parse(exampleXml));
+
+ // Return all of the markdown content before the examples begin
+ return markdown.Substring(0, match.Index);
+ }
+
+ return markdown;
+ }
+ }
+}
diff --git a/Libraries/Docs/DocsSummary.cs b/Libraries/Docs/DocsSummary.cs
new file mode 100644
index 0000000..45e4ade
--- /dev/null
+++ b/Libraries/Docs/DocsSummary.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace Libraries.Docs
+{
+ public class DocsSummary : DocsTextElement
+ {
+ public DocsSummary(XElement xeSummary) : base(xeSummary)
+ {
+ }
+ }
+}
diff --git a/Libraries/Docs/DocsTextElement.cs b/Libraries/Docs/DocsTextElement.cs
new file mode 100644
index 0000000..be4cf08
--- /dev/null
+++ b/Libraries/Docs/DocsTextElement.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Xml;
+using System.Xml.Linq;
+
+namespace Libraries.Docs
+{
+ public abstract class DocsTextElement
+ {
+ private readonly XElement Element;
+ private IEnumerable? _parsedNodes;
+ private string[]? _parsedLines;
+ private string? _parsedText;
+
+ public string RawText { get; private init; }
+
+ public IEnumerable RawNodes { get; private init; }
+
+ public IEnumerable ParsedNodes
+ {
+ get
+ {
+ EnsureParsed();
+ return _parsedNodes;
+ }
+ }
+
+ public string ParsedText
+ {
+ get
+ {
+ EnsureParsed();
+ return _parsedText;
+ }
+ }
+
+ public string[] ParsedTextLines
+ {
+ get
+ {
+ EnsureParsed();
+ return _parsedLines;
+ }
+ }
+
+ [MemberNotNull(nameof(_parsedNodes), nameof(_parsedText), nameof(_parsedLines))]
+ protected void EnsureParsed()
+ {
+ if (_parsedNodes is null || _parsedText is null || _parsedLines is null)
+ {
+ // Clone the element for non-mutating parsing
+ var cloned = XElement.Parse(Element.ToString()).Nodes();
+
+ // Parse each node and filter out nulls, building a block of text
+ _parsedNodes = cloned.Select(ParseNode).OfType().ToArray();
+ var allNodeContent = string.Join("", _parsedNodes);
+
+ // Normalize line endings, trim lines, remove empty lines, and join back into 1 string
+ allNodeContent = Regex.Replace(allNodeContent, "\r?\n", Environment.NewLine);
+ _parsedLines = allNodeContent.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ _parsedText = string.Join(Environment.NewLine, _parsedLines);
+ }
+ }
+
+ public DocsTextElement(XElement element)
+ {
+ Element = element;
+ RawNodes = element.Nodes();
+ RawText = string.Join("", RawNodes);
+ }
+
+ protected virtual string? ParseNode(XNode node) =>
+ RewriteDocReferences(node).ToString();
+
+ protected static XNode RewriteDocReferences(XNode node)
+ {
+ if (node is XElement element && element.Name == "see")
+ {
+ var cref = element.Attribute("cref");
+
+ if (cref is not null)
+ {
+ var apiReference = new DocsApiReference(cref.Value);
+ cref.SetValue(apiReference.Api);
+ }
+ }
+
+ return node;
+ }
+ }
+}
diff --git a/Libraries/Docs/DocsTextFormat.cs b/Libraries/Docs/DocsTextFormat.cs
new file mode 100644
index 0000000..8ee2782
--- /dev/null
+++ b/Libraries/Docs/DocsTextFormat.cs
@@ -0,0 +1,8 @@
+namespace Libraries.Docs
+{
+ public enum DocsTextFormat
+ {
+ PlainText,
+ Markdown
+ }
+}
diff --git a/Libraries/Docs/DocsType.cs b/Libraries/Docs/DocsType.cs
index 010faa9..ce9f833 100644
--- a/Libraries/Docs/DocsType.cs
+++ b/Libraries/Docs/DocsType.cs
@@ -9,7 +9,7 @@ namespace Libraries.Docs
///
/// Represents the root xml element (unique) of a Docs xml file, called Type.
///
- internal class DocsType : DocsAPI
+ public class DocsType : DocsAPI
{
private string? _typeName;
private string? _name;
diff --git a/Libraries/Docs/DocsTypeParam.cs b/Libraries/Docs/DocsTypeParam.cs
index af2ea7a..cf5f23d 100644
--- a/Libraries/Docs/DocsTypeParam.cs
+++ b/Libraries/Docs/DocsTypeParam.cs
@@ -6,7 +6,7 @@ namespace Libraries.Docs
///
/// Each one of these typeparam objects live inside the Docs section inside the Member object.
///
- internal class DocsTypeParam
+ public class DocsTypeParam : DocsTextElement
{
private readonly XElement XEDocsTypeParam;
public IDocsAPI ParentAPI
@@ -35,7 +35,7 @@ public string Value
}
}
- public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam)
+ public DocsTypeParam(IDocsAPI parentAPI, XElement xeDocsTypeParam) : base(xeDocsTypeParam)
{
ParentAPI = parentAPI;
XEDocsTypeParam = xeDocsTypeParam;
diff --git a/Libraries/Docs/DocsTypeParameter.cs b/Libraries/Docs/DocsTypeParameter.cs
index 73a2e1e..70364a1 100644
--- a/Libraries/Docs/DocsTypeParameter.cs
+++ b/Libraries/Docs/DocsTypeParameter.cs
@@ -7,7 +7,7 @@ namespace Libraries.Docs
///
/// Each one of these TypeParameter objects islocated inside the TypeParameters section inside the Member.
///
- internal class DocsTypeParameter
+ public class DocsTypeParameter
{
private readonly XElement XETypeParameter;
public string Name
@@ -56,6 +56,18 @@ public string ConstraintsBaseTypeName
}
}
+ public string ConstraintsInterfaceName
+ {
+ get
+ {
+ if (Constraints != null)
+ {
+ return XmlHelper.GetChildElementValue(Constraints, "InterfaceName");
+ }
+ return string.Empty;
+ }
+ }
+
public DocsTypeParameter(XElement xeTypeParameter)
{
XETypeParameter = xeTypeParameter;
diff --git a/Libraries/Docs/DocsTypeSignature.cs b/Libraries/Docs/DocsTypeSignature.cs
index 5ca5c46..1f78c72 100644
--- a/Libraries/Docs/DocsTypeSignature.cs
+++ b/Libraries/Docs/DocsTypeSignature.cs
@@ -2,7 +2,7 @@
namespace Libraries.Docs
{
- internal class DocsTypeSignature
+ public class DocsTypeSignature
{
private readonly XElement XETypeSignature;
diff --git a/Libraries/Docs/IDocsAPI.cs b/Libraries/Docs/IDocsAPI.cs
index afd4dd6..50bde84 100644
--- a/Libraries/Docs/IDocsAPI.cs
+++ b/Libraries/Docs/IDocsAPI.cs
@@ -3,7 +3,7 @@
namespace Libraries.Docs
{
- internal interface IDocsAPI
+ public interface IDocsAPI
{
public abstract APIKind Kind { get; }
public abstract bool IsUndocumented { get; }
diff --git a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
index 79970d2..f048be5 100644
--- a/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
+++ b/Libraries/IntelliSenseXml/IntelliSenseXmlMember.cs
@@ -165,8 +165,8 @@ public string Returns
{
if (_returns == null)
{
- XElement? xElement = XEMember.Element("returns");
- _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty;
+ XElement? xElement = XEMember.Element("returns");
+ _returns = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty;
}
return _returns;
}
@@ -179,8 +179,8 @@ public string Remarks
{
if (_remarks == null)
{
- XElement? xElement = XEMember.Element("remarks");
- _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty;
+ XElement? xElement = XEMember.Element("remarks");
+ _remarks = (xElement != null) ? XmlHelper.GetNodesInPlainText(xElement) : string.Empty;
}
return _remarks;
}
diff --git a/Libraries/Libraries.csproj b/Libraries/Libraries.csproj
index c898e37..75183c6 100644
--- a/Libraries/Libraries.csproj
+++ b/Libraries/Libraries.csproj
@@ -6,21 +6,19 @@
Microsoft
carlossanlop
enable
+ enable
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Libraries/Log.cs b/Libraries/Log.cs
index 2574a79..16ac859 100644
--- a/Libraries/Log.cs
+++ b/Libraries/Log.cs
@@ -18,10 +18,12 @@ public static void Print(bool endline, ConsoleColor foregroundColor, string form
string msg = args != null ? (args.Length > 0 ? string.Format(format, args) : format) : format;
if (endline)
{
+ Debug.WriteLine($"[DPT]: {msg}");
Console.WriteLine(msg);
}
else
{
+ Debug.Write(msg);
Console.Write(msg);
}
Console.ForegroundColor = originalColor;
diff --git a/Libraries/RoslynTripleSlash/LeadingTriviaRewriter.cs b/Libraries/RoslynTripleSlash/LeadingTriviaRewriter.cs
new file mode 100644
index 0000000..7630c99
--- /dev/null
+++ b/Libraries/RoslynTripleSlash/LeadingTriviaRewriter.cs
@@ -0,0 +1,181 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Libraries.RoslynTripleSlash
+{
+ public static class LeadingTriviaRewriter
+ {
+ private static int[] TriviaAboveDocComments = new[]
+ {
+ (int)SyntaxKind.RegionDirectiveTrivia,
+ (int)SyntaxKind.PragmaWarningDirectiveTrivia,
+ (int)SyntaxKind.IfDirectiveTrivia,
+ (int)SyntaxKind.EndIfDirectiveTrivia,
+ };
+
+ public static int[] TriviaBelowDocComments = new[]
+ {
+ (int)SyntaxKind.SingleLineCommentTrivia,
+ (int)SyntaxKind.MultiLineCommentTrivia
+ };
+
+ private static bool IsDocumentationCommentTrivia(this SyntaxTrivia trivia) =>
+ trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia) ||
+ trivia.IsKind(SyntaxKind.MultiLineDocumentationCommentTrivia);
+
+ private static bool IsDocumentationCommentTriviaContinuation(this SyntaxTrivia trivia) =>
+ trivia.IsDocumentationCommentTrivia() ||
+ trivia.IsKind(SyntaxKind.EndOfLineTrivia) ||
+ trivia.IsKind(SyntaxKind.WhitespaceTrivia);
+
+ public static SyntaxTriviaList WithoutDocumentationComments(this SyntaxTriviaList trivia)
+ {
+ return trivia.WithoutDocumentationComments(out int? _);
+ }
+
+ public static SyntaxTriviaList GetFinalWhitespace(this SyntaxTriviaList trivia)
+ {
+ SyntaxTriviaList indentation = new();
+ int index = trivia.Count;
+
+ while (index > 0 && trivia[index - 1].IsKind(SyntaxKind.WhitespaceTrivia))
+ {
+ index--;
+ indentation = indentation.Insert(0, trivia[index]);
+ }
+
+ return indentation;
+ }
+
+ public static SyntaxTriviaList WithoutDocumentationComments(this SyntaxTriviaList trivia, out int? existingDocsPosition)
+ {
+ int i = 0;
+ existingDocsPosition = null;
+
+ // Before we start removing the doc comments, we need to capture any whitespace at
+ // the very end of the trivia, because it could represent indentation of the API.
+ SyntaxTriviaList indentation = trivia.GetFinalWhitespace();
+
+ while (i < trivia.Count)
+ {
+ if (trivia[i].IsDocumentationCommentTrivia())
+ {
+ var commentStart = i;
+ var commentEnd = i;
+
+ // Walk backward through whitespace to find the beginning of the doc comment
+ // Now walk the doc comment position backward through any of its indentation trivia
+ while (commentStart > 0 && trivia[commentStart - 1].IsKind(SyntaxKind.WhitespaceTrivia))
+ {
+ commentStart--;
+ }
+
+ // Walk forward to find the end of the doc comment, but do not go past the
+ // beginning of the API documentation.
+ while (commentEnd < trivia.Count - indentation.Count && trivia[commentEnd + 1].IsDocumentationCommentTriviaContinuation())
+ {
+ commentEnd++;
+ }
+
+ // Finally, walk the end position backthrough any indentation (for other
+ // lines before the API itself).
+ while (commentEnd >= commentStart && trivia[commentEnd].IsKind(SyntaxKind.WhitespaceTrivia))
+ {
+ commentEnd--;
+ }
+
+ // Remove the trivia from beginning to end of this doc comment
+ while (commentEnd >= commentStart)
+ {
+ trivia = trivia.RemoveAt(commentStart);
+ commentEnd--;
+ }
+
+ // Capture the first documentation comment position
+ // If there were disjoint doc comments, we will
+ // anchor on the first occurrence
+ existingDocsPosition ??= commentStart;
+ }
+
+ i++;
+ }
+
+ return trivia;
+ }
+
+ public static SyntaxNode ApplyXmlComments(SyntaxNode node, IEnumerable xmlComments)
+ {
+ if (!node.HasLeadingTrivia)
+ {
+ return node.WithLeadingTrivia(GetXmlCommentLines(xmlComments));
+ }
+
+ SyntaxTriviaList leading = node.GetLeadingTrivia().WithoutDocumentationComments(out int? docsPosition);
+
+ if (docsPosition is null)
+ {
+ // We will determine the position at which to insert the XML
+ // comments. We want to find the spot closest to the declaration
+ // that makes sense, so we walk upward through the leading trivia
+ // until we find nodes we need to stay beneath. Then, we walk back
+ // downward until we find the first node to stay above.
+ docsPosition = leading.Count;
+
+ while (docsPosition > 0 && !TriviaAboveDocComments.Contains(leading[docsPosition.Value - 1].RawKind))
+ {
+ docsPosition--;
+ }
+
+ while (docsPosition < leading.Count && !TriviaBelowDocComments.Contains(leading[docsPosition.Value].RawKind))
+ {
+ docsPosition++;
+ }
+
+ // The last step is to walk backwards again through any whitespace
+ // to get back to the beginning of the line.
+ while (docsPosition > 0 && leading[docsPosition.Value - 1].IsKind(SyntaxKind.WhitespaceTrivia))
+ {
+ docsPosition--;
+ }
+ }
+
+ // We know where the doc comments will be inserted, but they could go in adjacent to
+ // pragmas or other trivia where the indentation might not match the API being
+ // documented. Look at the end of the trivia (just before the API), and clone the
+ // indentation for use in front of each line of documentation comments.
+ SyntaxTriviaList indentation = leading.GetFinalWhitespace();
+
+ // Insert the XML comment lines with the collected indentation
+ return node.WithLeadingTrivia(
+ leading.InsertRange(docsPosition.Value, GetXmlCommentLines(xmlComments, indentation))
+ );
+ }
+
+ public static SyntaxTriviaList GetXmlCommentLines(IEnumerable xmlComments, SyntaxTriviaList indentation = new())
+ {
+ SyntaxTriviaList xmlTrivia = new();
+
+ foreach (var xmlComment in xmlComments)
+ {
+ var lines = xmlComment.ToString().Split(Environment.NewLine);
+
+ var commentLines = lines.Select((l, i) => {
+ var text = XmlText(XmlTextLiteral(l, l));
+ var comment = DocumentationComment(text);
+ var leadingTrivia = comment.GetLeadingTrivia().InsertRange(0, indentation);
+
+ return Trivia(comment.WithLeadingTrivia(leadingTrivia).WithTrailingTrivia(CarriageReturnLineFeed));
+ });
+
+ xmlTrivia = xmlTrivia.AddRange(commentLines);
+ }
+
+ return xmlTrivia;
+ }
+ }
+}
diff --git a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
index ec8264f..7aa8271 100644
--- a/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
+++ b/Libraries/RoslynTripleSlash/TripleSlashSyntaxRewriter.cs
@@ -20,7 +20,7 @@ namespace Libraries.RoslynTripleSlash
/// My param description.
/// My remarks.
public ...
-
+
translates to this syntax tree structure:
PublicKeyword (SyntaxToken) -> The public keyword including its trivia.
@@ -88,63 +88,9 @@ public ...
*/
internal class TripleSlashSyntaxRewriter : CSharpSyntaxRewriter
{
- #region Private members
-
- private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" };
-
- private static readonly string[] MarkdownUnconvertableStrings = new[] { "](~/includes", "[!INCLUDE" };
-
- private static readonly string[] MarkdownCodeIncludes = new[] { "[!code-cpp", "[!code-csharp", "[!code-vb", };
-
- private static readonly string[] MarkdownExamples = new[] { "## Examples", "## Example" };
-
- private static readonly string[] MarkdownHeaders = new[] { "[!NOTE]", "[!IMPORTANT]", "[!TIP]" };
-
- // Note that we need to support generics that use the ` literal as well as the escaped %60
- private static readonly string ValidRegexChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;]|(%60|`)\d+";
- private static readonly string ValidExtraChars = @"\?=";
-
- private static readonly string RegexDocIdPattern = @"(?[A-Za-z]{1}:)?(?(" + ValidRegexChars + @")+)(?%2[aA])?(?\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?";
- private static readonly string RegexXmlCrefPattern = "cref=\"" + RegexDocIdPattern + "\"";
- private static readonly string RegexMarkdownXrefPattern = @"(?)";
-
- private static readonly string RegexMarkdownBoldPattern = @"\*\*(?[A-Za-z0-9\-\._~:\/#\[\]@!\$&'\(\)\+,;%` ]+)\*\*";
- private static readonly string RegexXmlBoldReplacement = @"${content}";
-
- private static readonly string RegexMarkdownLinkPattern = @"\[(?.+)\]\((?(http|www)(" + ValidRegexChars + "|" + ValidExtraChars + @")+)\)";
- private static readonly string RegexHtmlLinkReplacement = "${linkValue}";
-
- private static readonly string RegexMarkdownCodeStartPattern = @"```(?(cs|csharp|cpp|vb|visualbasic))(?\s+)";
- private static readonly string RegexXmlCodeStartReplacement = "${spaces}";
-
- private static readonly string RegexMarkdownCodeEndPattern = @"```(?\s+)";
- private static readonly string RegexXmlCodeEndReplacement = "
${spaces}";
-
- private static readonly Dictionary PrimitiveTypes = new()
- {
- { "System.Boolean", "bool" },
- { "System.Byte", "byte" },
- { "System.Char", "char" },
- { "System.Decimal", "decimal" },
- { "System.Double", "double" },
- { "System.Int16", "short" },
- { "System.Int32", "int" },
- { "System.Int64", "long" },
- { "System.Object", "object" }, // Ambiguous: could be 'object' or 'dynamic' https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types
- { "System.SByte", "sbyte" },
- { "System.Single", "float" },
- { "System.String", "string" },
- { "System.UInt16", "ushort" },
- { "System.UInt32", "uint" },
- { "System.UInt64", "ulong" },
- { "System.Void", "void" }
- };
-
private DocsCommentsContainer DocsComments { get; }
private SemanticModel Model { get; }
- #endregion
-
public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticModel model) : base(visitIntoStructuredTrivia: true)
{
DocsComments = docsComments;
@@ -234,17 +180,17 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod
return node;
}
- SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+ List xmlComments = new();
+ xmlComments.Add(XmlDocComments.GetSummary(member.SummaryElement));
+ xmlComments.Add(XmlDocComments.GetValue(member.Value));
+ xmlComments.AddRange(XmlDocComments.GetExceptions(member.Exceptions));
+ xmlComments.Add(XmlDocComments.GetRemarks(member.RemarksElement));
+ xmlComments.AddRange(XmlDocComments.GetExamples(member.ExampleElements));
+ xmlComments.AddRange(XmlDocComments.GetSeeAlsos(member.SeeAlsoCrefs));
+ xmlComments.AddRange(XmlDocComments.GetAltMembers(member.AltMembers));
+ xmlComments.AddRange(XmlDocComments.GetRelateds(member.Relateds));
- SyntaxTriviaList summary = GetSummary(member, leadingWhitespace);
- SyntaxTriviaList value = GetValue(member, leadingWhitespace);
- SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace);
- SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace);
- SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace);
- SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace);
- SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
-
- return GetNodeWithTrivia(leadingWhitespace, node, summary, value, exceptions, remarks, seealsos, altmembers, relateds);
+ return LeadingTriviaRewriter.ApplyXmlComments(node, xmlComments.Where(c => c is not null)!);
}
public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node)
@@ -293,23 +239,22 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod
return node;
}
- SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
-
if (!TryGetType(symbol, out DocsType? type))
{
return node;
}
- SyntaxTriviaList summary = GetSummary(type, leadingWhitespace);
- SyntaxTriviaList typeParameters = GetTypeParameters(type, leadingWhitespace);
- SyntaxTriviaList parameters = GetParameters(type, leadingWhitespace);
- SyntaxTriviaList remarks = GetRemarks(type, leadingWhitespace);
- SyntaxTriviaList seealsos = GetSeeAlsos(type.SeeAlsoCrefs, leadingWhitespace);
- SyntaxTriviaList altmembers = GetAltMembers(type.AltMembers, leadingWhitespace);
- SyntaxTriviaList relateds = GetRelateds(type.Relateds, leadingWhitespace);
-
+ List xmlComments = new();
+ xmlComments.Add(XmlDocComments.GetSummary(type.SummaryElement));
+ xmlComments.AddRange(XmlDocComments.GetTypeParameters(type.TypeParams));
+ xmlComments.AddRange(XmlDocComments.GetParameters(type.Params));
+ xmlComments.Add(XmlDocComments.GetRemarks(type.RemarksElement));
+ xmlComments.AddRange(XmlDocComments.GetExamples(type.ExampleElements));
+ xmlComments.AddRange(XmlDocComments.GetSeeAlsos(type.SeeAlsoCrefs));
+ xmlComments.AddRange(XmlDocComments.GetAltMembers(type.AltMembers));
+ xmlComments.AddRange(XmlDocComments.GetRelateds(type.Relateds));
- return GetNodeWithTrivia(leadingWhitespace, node, summary, typeParameters, parameters, remarks, seealsos, altmembers, relateds);
+ return LeadingTriviaRewriter.ApplyXmlComments(node, xmlComments.Where(c => c is not null)!);
}
private SyntaxNode? VisitBaseMethodDeclaration(BaseMethodDeclarationSyntax node)
@@ -321,19 +266,19 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod
return node;
}
- SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+ List xmlComments = new();
+ xmlComments.Add(XmlDocComments.GetSummary(member.SummaryElement));
+ xmlComments.AddRange(XmlDocComments.GetTypeParameters(member.TypeParams));
+ xmlComments.AddRange(XmlDocComments.GetParameters(member.Params));
+ xmlComments.Add(XmlDocComments.GetReturns(member.Returns));
+ xmlComments.AddRange(XmlDocComments.GetExceptions(member.Exceptions));
+ xmlComments.Add(XmlDocComments.GetRemarks(member.RemarksElement));
+ xmlComments.AddRange(XmlDocComments.GetExamples(member.ExampleElements));
+ xmlComments.AddRange(XmlDocComments.GetSeeAlsos(member.SeeAlsoCrefs));
+ xmlComments.AddRange(XmlDocComments.GetAltMembers(member.AltMembers));
+ xmlComments.AddRange(XmlDocComments.GetRelateds(member.Relateds));
- SyntaxTriviaList summary = GetSummary(member, leadingWhitespace);
- SyntaxTriviaList typeParameters = GetTypeParameters(member, leadingWhitespace);
- SyntaxTriviaList parameters = GetParameters(member, leadingWhitespace);
- SyntaxTriviaList returns = GetReturns(member, leadingWhitespace);
- SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace);
- SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace);
- SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace);
- SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace);
- SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
-
- return GetNodeWithTrivia(leadingWhitespace, node, summary, typeParameters, parameters, returns, exceptions, remarks, seealsos, altmembers, relateds);
+ return LeadingTriviaRewriter.ApplyXmlComments(node, xmlComments.Where(c => c is not null)!);
}
private SyntaxNode? VisitMemberDeclaration(MemberDeclarationSyntax node)
@@ -343,16 +288,16 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod
return node;
}
- SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
-
- SyntaxTriviaList summary = GetSummary(member, leadingWhitespace);
- SyntaxTriviaList exceptions = GetExceptions(member.Exceptions, leadingWhitespace);
- SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace);
- SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace);
- SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace);
- SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
+ List xmlComments = new();
+ xmlComments.Add(XmlDocComments.GetSummary(member.SummaryElement));
+ xmlComments.AddRange(XmlDocComments.GetExceptions(member.Exceptions));
+ xmlComments.Add(XmlDocComments.GetRemarks(member.RemarksElement));
+ xmlComments.AddRange(XmlDocComments.GetExamples(member.ExampleElements));
+ xmlComments.AddRange(XmlDocComments.GetSeeAlsos(member.SeeAlsoCrefs));
+ xmlComments.AddRange(XmlDocComments.GetAltMembers(member.AltMembers));
+ xmlComments.AddRange(XmlDocComments.GetRelateds(member.Relateds));
- return GetNodeWithTrivia(leadingWhitespace, node, summary, exceptions, remarks, seealsos, altmembers, relateds);
+ return LeadingTriviaRewriter.ApplyXmlComments(node, xmlComments.Where(c => c is not null)!);
}
private SyntaxNode? VisitVariableDeclaration(BaseFieldDeclarationSyntax node)
@@ -368,15 +313,15 @@ public TripleSlashSyntaxRewriter(DocsCommentsContainer docsComments, SemanticMod
return node;
}
- SyntaxTriviaList leadingWhitespace = GetLeadingWhitespace(node);
+ List xmlComments = new();
+ xmlComments.Add(XmlDocComments.GetSummary(member.SummaryElement));
+ xmlComments.Add(XmlDocComments.GetRemarks(member.RemarksElement));
+ xmlComments.AddRange(XmlDocComments.GetExamples(member.ExampleElements));
+ xmlComments.AddRange(XmlDocComments.GetSeeAlsos(member.SeeAlsoCrefs));
+ xmlComments.AddRange(XmlDocComments.GetAltMembers(member.AltMembers));
+ xmlComments.AddRange(XmlDocComments.GetRelateds(member.Relateds));
- SyntaxTriviaList summary = GetSummary(member, leadingWhitespace);
- SyntaxTriviaList remarks = GetRemarks(member, leadingWhitespace);
- SyntaxTriviaList seealsos = GetSeeAlsos(member.SeeAlsoCrefs, leadingWhitespace);
- SyntaxTriviaList altmembers = GetAltMembers(member.AltMembers, leadingWhitespace);
- SyntaxTriviaList relateds = GetRelateds(member.Relateds, leadingWhitespace);
-
- return GetNodeWithTrivia(leadingWhitespace, node, summary, remarks, seealsos, altmembers, relateds);
+ return LeadingTriviaRewriter.ApplyXmlComments(node, xmlComments.Where(c => c is not null)!);
}
return node;
@@ -411,594 +356,5 @@ private bool TryGetType(ISymbol symbol, [NotNullWhen(returnValue: true)] out Doc
}
#endregion
-
- #region Syntax manipulation
-
- private static SyntaxNode GetNodeWithTrivia(SyntaxTriviaList leadingWhitespace, SyntaxNode node, params SyntaxTriviaList[] trivias)
- {
- SyntaxTriviaList leadingDoubleSlashComments = GetLeadingDoubleSlashComments(node, leadingWhitespace);
-
- SyntaxTriviaList finalTrivia = new();
- foreach (SyntaxTriviaList t in trivias)
- {
- finalTrivia = finalTrivia.AddRange(t);
- }
- finalTrivia = finalTrivia.AddRange(leadingDoubleSlashComments);
-
- if (finalTrivia.Count > 0)
- {
- finalTrivia = finalTrivia.AddRange(leadingWhitespace);
-
- var leadingTrivia = node.GetLeadingTrivia();
- if (leadingTrivia.Any())
- {
- if (leadingTrivia[0].IsKind(SyntaxKind.EndOfLineTrivia))
- {
- // Ensure the endline that separates nodes is respected
- finalTrivia = new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed)
- .AddRange(finalTrivia);
- }
- }
-
- return node.WithLeadingTrivia(finalTrivia);
- }
-
- // If there was no new trivia, return untouched
- return node;
- }
-
- // Finds the last set of whitespace characters that are to the left of the public|protected keyword of the node.
- private static SyntaxTriviaList GetLeadingWhitespace(SyntaxNode node)
- {
- SyntaxTriviaList triviaList = GetLeadingTrivia(node);
-
- if (triviaList.Any() &&
- triviaList.LastOrDefault(t => t.IsKind(SyntaxKind.WhitespaceTrivia)) is SyntaxTrivia last)
- {
- return new(last);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetLeadingDoubleSlashComments(SyntaxNode node, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList triviaList = GetLeadingTrivia(node);
-
- SyntaxTriviaList doubleSlashComments = new();
-
- foreach (SyntaxTrivia trivia in triviaList)
- {
- if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
- {
- doubleSlashComments = doubleSlashComments
- .AddRange(leadingWhitespace)
- .Add(trivia)
- .Add(SyntaxFactory.CarriageReturnLineFeed);
- }
- }
-
- return doubleSlashComments;
- }
-
- private static SyntaxTriviaList GetLeadingTrivia(SyntaxNode node)
- {
- if (node is MemberDeclarationSyntax memberDeclaration)
- {
- if ((memberDeclaration.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.PublicKeyword) || x.IsKind(SyntaxKind.ProtectedKeyword)) is SyntaxToken modifier) &&
- !modifier.IsKind(SyntaxKind.None))
- {
- return modifier.LeadingTrivia;
- }
-
- return node.GetLeadingTrivia();
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetSummary(DocsAPI api, SyntaxTriviaList leadingWhitespace)
- {
- if (!api.Summary.IsDocsEmpty())
- {
- XmlTextSyntax contents = GetTextAsCommentedTokens(api.Summary, leadingWhitespace);
- XmlElementSyntax element = SyntaxFactory.XmlSummaryElement(contents);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leadingWhitespace)
- {
- if (!api.Remarks.IsDocsEmpty())
- {
- return GetFormattedRemarks(api, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace)
- {
- if (!api.Value.IsDocsEmpty())
- {
- XmlTextSyntax contents = GetTextAsCommentedTokens(api.Value, leadingWhitespace);
- XmlElementSyntax element = SyntaxFactory.XmlValueElement(contents);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace)
- {
- if (!text.IsDocsEmpty())
- {
- XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
- XmlElementSyntax element = SyntaxFactory.XmlParamElement(name, contents);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList parameters = new();
- foreach (SyntaxTriviaList parameterTrivia in api.Params
- .Where(param => !param.Value.IsDocsEmpty())
- .Select(param => GetParameter(param.Name, param.Value, leadingWhitespace)))
- {
- parameters = parameters.AddRange(parameterTrivia);
- }
- return parameters;
- }
-
- private static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace)
- {
- if (!text.IsDocsEmpty())
- {
- var attribute = new SyntaxList(SyntaxFactory.XmlTextAttribute("name", name));
- XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
- return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList typeParameters = new();
- foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams
- .Where(typeParam => !typeParam.Value.IsDocsEmpty())
- .Select(typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace)))
- {
- typeParameters = typeParameters.AddRange(typeParameterTrivia);
- }
- return typeParameters;
- }
-
- private static SyntaxTriviaList GetReturns(DocsMember api, SyntaxTriviaList leadingWhitespace)
- {
- // Also applies for when is empty because the method return type is void
- if (!api.Returns.IsDocsEmpty())
- {
- XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace);
- XmlElementSyntax element = SyntaxFactory.XmlReturnsElement(contents);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace)
- {
- if (!text.IsDocsEmpty())
- {
- cref = RemoveCrefPrefix(cref);
- TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref));
- XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
- XmlElementSyntax element = SyntaxFactory.XmlExceptionElement(crefSyntax, contents);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- return new();
- }
-
- private static SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList exceptions = new();
- if (docsExceptions.Any())
- {
- foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select(
- exception => GetException(exception.Cref, exception.Value, leadingWhitespace)))
- {
- exceptions = exceptions.AddRange(exceptionsTrivia);
- }
- }
- return exceptions;
- }
-
- private static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace)
- {
- cref = RemoveCrefPrefix(cref);
- TypeCrefSyntax crefSyntax = SyntaxFactory.TypeCref(SyntaxFactory.ParseTypeName(cref));
- XmlEmptyElementSyntax element = SyntaxFactory.XmlSeeAlsoElement(crefSyntax);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- private static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList seealsos = new();
- if (docsSeeAlsoCrefs.Any())
- {
- foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select(
- s => GetSeeAlso(s, leadingWhitespace)))
- {
- seealsos = seealsos.AddRange(seealsoTrivia);
- }
- }
- return seealsos;
- }
-
- private static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace)
- {
- cref = RemoveCrefPrefix(cref);
- XmlAttributeSyntax attribute = SyntaxFactory.XmlTextAttribute("cref", cref);
- XmlEmptyElementSyntax emptyElement = SyntaxFactory.XmlEmptyElement(SyntaxFactory.XmlName(SyntaxFactory.Identifier("altmember")), new SyntaxList(attribute));
- return GetXmlTrivia(emptyElement, leadingWhitespace);
- }
-
- private static SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList altMembers = new();
- if (docsAltMembers.Any())
- {
- foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select(
- s => GetAltMember(s, leadingWhitespace)))
- {
- altMembers = altMembers.AddRange(altMemberTrivia);
- }
- }
- return altMembers;
- }
-
- private static SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxList attributes = new();
-
- attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("type", articleType));
- attributes = attributes.Add(SyntaxFactory.XmlTextAttribute("href", href));
-
- XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace);
- return GetXmlTrivia("related", attributes, contents, leadingWhitespace);
- }
-
- private static SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace)
- {
- SyntaxTriviaList relateds = new();
- if (docsRelateds.Any())
- {
- foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select(
- s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace)))
- {
- relateds = relateds.AddRange(relatedsTrivia);
- }
- }
- return relateds;
- }
-
- private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool wrapWithNewLines = false)
- {
- text = CleanCrefs(text);
-
- // collapse newlines to a single one
- string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", "");
- SyntaxToken whitespaceToken = SyntaxFactory.XmlTextNewLine(Environment.NewLine + whitespace);
-
- SyntaxTrivia leadingTrivia = SyntaxFactory.SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty);
- SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia);
-
- string[] lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
-
- var tokens = new List();
-
- if (wrapWithNewLines)
- {
- tokens.Add(whitespaceToken);
- }
-
- for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++)
- {
- string line = lines[lineNumber];
-
- SyntaxToken token = SyntaxFactory.XmlTextLiteral(leading, line, line, default);
- tokens.Add(token);
-
- if (lines.Length > 1 && lineNumber < lines.Length - 1)
- {
- tokens.Add(whitespaceToken);
- }
- }
-
- if (wrapWithNewLines)
- {
- tokens.Add(whitespaceToken);
- }
-
- XmlTextSyntax xmlText = SyntaxFactory.XmlText(tokens.ToArray());
- return xmlText;
- }
-
- private static SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace)
- {
- DocumentationCommentTriviaSyntax docComment = SyntaxFactory.DocumentationComment(node);
- SyntaxTrivia docCommentTrivia = SyntaxFactory.Trivia(docComment);
-
- return leadingWhitespace
- .Add(docCommentTrivia)
- .Add(SyntaxFactory.CarriageReturnLineFeed);
- }
-
- // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes.
- // Looks like below (excluding square brackets):
- // [ /// text]
- private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace)
- {
- XmlElementStartTagSyntax start = SyntaxFactory.XmlElementStartTag(
- SyntaxFactory.Token(SyntaxKind.LessThanToken),
- SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)),
- attributes,
- SyntaxFactory.Token(SyntaxKind.GreaterThanToken));
-
- XmlElementEndTagSyntax end = SyntaxFactory.XmlElementEndTag(
- SyntaxFactory.Token(SyntaxKind.LessThanSlashToken),
- SyntaxFactory.XmlName(SyntaxFactory.Identifier(name)),
- SyntaxFactory.Token(SyntaxKind.GreaterThanToken));
-
- XmlElementSyntax element = SyntaxFactory.XmlElement(start, new SyntaxList(contents), end);
- return GetXmlTrivia(element, leadingWhitespace);
- }
-
- private static string WrapInRemarks(string acum)
- {
- string wrapped = Environment.NewLine + "" + Environment.NewLine;
- return wrapped;
- }
-
- private static string WrapCodeIncludes(string[] splitted, ref int n)
- {
- string acum = string.Empty;
- while (n < splitted.Length && splitted[n].ContainsStrings(MarkdownCodeIncludes))
- {
- acum += Environment.NewLine + splitted[n];
- if ((n + 1) < splitted.Length && splitted[n + 1].ContainsStrings(MarkdownCodeIncludes))
- {
- n++;
- }
- else
- {
- break;
- }
- }
- return WrapInRemarks(acum);
- }
-
- private static SyntaxTriviaList GetFormattedRemarks(IDocsAPI api, SyntaxTriviaList leadingWhitespace)
- {
-
- string remarks = RemoveUnnecessaryMarkdown(api.Remarks);
- string example = string.Empty;
-
- XmlNodeSyntax contents;
- if (remarks.ContainsStrings(MarkdownUnconvertableStrings))
- {
- contents = GetTextAsFormatCData(remarks, leadingWhitespace);
- }
- else
- {
- string[] splitted = remarks.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
- string updatedRemarks = string.Empty;
- for (int n = 0; n < splitted.Length; n++)
- {
- string acum;
- string line = splitted[n];
- if (line.ContainsStrings(MarkdownHeaders))
- {
- acum = line;
- n++;
- while (n < splitted.Length && splitted[n].StartsWith(">"))
- {
- acum += Environment.NewLine + splitted[n];
- if ((n + 1) < splitted.Length && splitted[n + 1].StartsWith(">"))
- {
- n++;
- }
- else
- {
- break;
- }
- }
- updatedRemarks += WrapInRemarks(acum);
- }
- else if (line.ContainsStrings(MarkdownCodeIncludes))
- {
- updatedRemarks += WrapCodeIncludes(splitted, ref n);
- }
- // When an example is found, everything after the header is considered part of that section
- else if (line.Contains("## Example"))
- {
- n++;
- while (n < splitted.Length)
- {
- line = splitted[n];
- if (line.ContainsStrings(MarkdownCodeIncludes))
- {
- example += WrapCodeIncludes(splitted, ref n);
- }
- else
- {
- example += Environment.NewLine + line;
- }
- n++;
- }
- }
- else
- {
- updatedRemarks += ReplaceMarkdownWithXmlElements(Environment.NewLine + line, api.Params, api.TypeParams);
- }
- }
-
- contents = GetTextAsCommentedTokens(updatedRemarks, leadingWhitespace);
- }
-
- XmlElementSyntax remarksXml = SyntaxFactory.XmlRemarksElement(contents);
- SyntaxTriviaList result = GetXmlTrivia(remarksXml, leadingWhitespace);
-
- if (!string.IsNullOrWhiteSpace(example))
- {
- SyntaxTriviaList exampleTriviaList = GetFormattedExamples(api, example, leadingWhitespace);
- result = result.AddRange(exampleTriviaList);
- }
-
- return result;
- }
-
- private static SyntaxTriviaList GetFormattedExamples(IDocsAPI api, string example, SyntaxTriviaList leadingWhitespace)
- {
- example = ReplaceMarkdownWithXmlElements(example, api.Params, api.TypeParams);
- XmlNodeSyntax exampleContents = GetTextAsCommentedTokens(example, leadingWhitespace);
- XmlElementSyntax exampleXml = SyntaxFactory.XmlExampleElement(exampleContents);
- SyntaxTriviaList exampleTriviaList = GetXmlTrivia(exampleXml, leadingWhitespace);
- return exampleTriviaList;
- }
-
- private static XmlNodeSyntax GetTextAsFormatCData(string text, SyntaxTriviaList leadingWhitespace)
- {
- XmlTextSyntax remarks = GetTextAsCommentedTokens(text, leadingWhitespace, wrapWithNewLines: true);
-
- XmlNameSyntax formatName = SyntaxFactory.XmlName("format");
- XmlAttributeSyntax formatAttribute = SyntaxFactory.XmlTextAttribute("type", "text/markdown");
- var formatAttributes = new SyntaxList(formatAttribute);
-
- var formatStart = SyntaxFactory.XmlElementStartTag(formatName, formatAttributes);
- var formatEnd = SyntaxFactory.XmlElementEndTag(formatName);
-
- XmlCDataSectionSyntax cdata = SyntaxFactory.XmlCDataSection(remarks.TextTokens);
- var cdataList = new SyntaxList(cdata);
-
- XmlElementSyntax contents = SyntaxFactory.XmlElement(formatStart, cdataList, formatEnd);
-
- return contents;
- }
-
- private static string RemoveUnnecessaryMarkdown(string text)
- {
- text = Regex.Replace(text, @"", "");
- text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", "");
- return text;
- }
-
- private static string ReplaceMarkdownWithXmlElements(string text, List docsParams, List docsTypeParams)
- {
- text = CleanXrefs(text);
-
- // commonly used url entities
- text = Regex.Replace(text, @"%23", "#");
- text = Regex.Replace(text, @"%28", "(");
- text = Regex.Replace(text, @"%29", ")");
- text = Regex.Replace(text, @"%2C", ",");
-
- // hyperlinks
- text = Regex.Replace(text, RegexMarkdownLinkPattern, RegexHtmlLinkReplacement);
-
- // bold
- text = Regex.Replace(text, RegexMarkdownBoldPattern, RegexXmlBoldReplacement);
-
- // code snippet
- text = Regex.Replace(text, RegexMarkdownCodeStartPattern, RegexXmlCodeStartReplacement);
- text = Regex.Replace(text, RegexMarkdownCodeEndPattern, RegexXmlCodeEndReplacement);
-
- // langwords|parameters|typeparams
- MatchCollection collection = Regex.Matches(text, @"(?`(?[a-zA-Z0-9_]+)`)");
- foreach (Match match in collection)
- {
- string backtickedParam = match.Groups["backtickedParam"].Value;
- string paramName = match.Groups["paramName"].Value;
- if (ReservedKeywords.Any(x => x == paramName))
- {
- text = Regex.Replace(text, $"{backtickedParam}", $"");
- }
- else if (docsParams.Any(x => x.Name == paramName))
- {
- text = Regex.Replace(text, $"{backtickedParam}", $"");
- }
- else if (docsTypeParams.Any(x => x.Name == paramName))
- {
- text = Regex.Replace(text, $"{backtickedParam}", $"");
- }
- }
-
- return text;
- }
-
- // Removes the one letter prefix and the following colon, if found, from a cref.
- private static string RemoveCrefPrefix(string cref)
- {
- if (cref.Length > 2 && cref[1] == ':')
- {
- return cref[2..];
- }
- return cref;
- }
-
- private static string ReplacePrimitives(string text)
- {
- foreach ((string key, string value) in PrimitiveTypes)
- {
- text = Regex.Replace(text, key, value);
- }
- return text;
- }
-
- private static string ReplaceDocId(Match m)
- {
- string docId = m.Groups["docId"].Value;
- string overload = string.IsNullOrWhiteSpace(m.Groups["overload"].Value) ? "" : "O:";
- docId = ReplacePrimitives(docId);
- docId = Regex.Replace(docId, @"%60", "`");
- docId = Regex.Replace(docId, @"`\d", "{T}");
- return overload + docId;
- }
-
- private static string CrefEvaluator(Match m)
- {
- string docId = ReplaceDocId(m);
- return "cref=\"" + docId + "\"";
- }
-
- private static string CleanCrefs(string text)
- {
- text = Regex.Replace(text, RegexXmlCrefPattern, CrefEvaluator);
- return text;
- }
-
- private static string XrefEvaluator(Match m)
- {
- string docId = ReplaceDocId(m);
- return "";
- }
-
- private static string CleanXrefs(string text)
- {
- text = Regex.Replace(text, RegexMarkdownXrefPattern, XrefEvaluator);
- return text;
- }
-
- #endregion
}
}
diff --git a/Libraries/RoslynTripleSlash/XmlDocComments.cs b/Libraries/RoslynTripleSlash/XmlDocComments.cs
new file mode 100644
index 0000000..78736da
--- /dev/null
+++ b/Libraries/RoslynTripleSlash/XmlDocComments.cs
@@ -0,0 +1,800 @@
+using Libraries.Docs;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace Libraries.RoslynTripleSlash
+{
+ public static class XmlDocComments
+ {
+ private static readonly string[] ReservedKeywords = new[] { "abstract", "async", "await", "false", "null", "sealed", "static", "true", "virtual" };
+
+ private static readonly Dictionary PrimitiveTypes = new()
+ {
+ { "System.Boolean", "bool" },
+ { "System.Byte", "byte" },
+ { "System.Char", "char" },
+ { "System.Decimal", "decimal" },
+ { "System.Double", "double" },
+ { "System.Int16", "short" },
+ { "System.Int32", "int" },
+ { "System.Int64", "long" },
+ { "System.Object", "object" }, // Ambiguous: could be 'object' or 'dynamic' https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types
+ { "System.SByte", "sbyte" },
+ { "System.Single", "float" },
+ { "System.String", "string" },
+ { "System.UInt16", "ushort" },
+ { "System.UInt32", "uint" },
+ { "System.UInt64", "ulong" },
+ { "System.Void", "void" }
+ };
+
+ // Note that we need to support generics that use the ` literal as well as any url encoded character
+ private static readonly string ValidRegexChars = @"[A-Za-z0-9\-\._~:\/#\[\]\{\}@!\$&'\(\)\*\+,;]|`\d+|%\w{2}";
+ private static readonly string ValidExtraChars = @"\?=";
+
+ private static readonly string RegexDocIdPattern = @"(?[A-Za-z]{1}:)?(?(" + ValidRegexChars + @")+)?(?\?(" + ValidRegexChars + @")+=(" + ValidRegexChars + @")+)?";
+ private static readonly string RegexXmlCrefPattern = "cref=\"" + RegexDocIdPattern + "\"";
+ private static readonly string RegexMarkdownXrefPattern = @"(?)";
+
+ private static readonly string RegexMarkdownBoldPattern = @"\*\*(?[A-Za-z0-9\-\._~:\/#\[\]@!\$&'\(\)\+,;%` ]+)\*\*";
+ private static readonly string RegexXmlBoldReplacement = @"${content}";
+
+ private static readonly string RegexMarkdownLinkPattern = @"\[(?.+)\]\((?(http|www)(" + ValidRegexChars + "|" + ValidExtraChars + @")+)\)";
+ private static readonly string RegexHtmlLinkReplacement = "${linkValue}";
+
+ private static readonly string RegexMarkdownCodeStartPattern = @"```(?(cs|csharp|cpp|vb|visualbasic))(?\s+)";
+ private static readonly string RegexXmlCodeStartReplacement = "${spaces}";
+
+ private static readonly string RegexMarkdownCodeEndPattern = @"```(?\s+)";
+ private static readonly string RegexXmlCodeEndReplacement = "
${spaces}";
+ private static readonly string[] MarkdownUnconvertableStrings = new[] { "](~/includes", "[!INCLUDE" };
+
+ private static readonly string[] MarkdownCodeIncludes = new[] { "[!code-cpp", "[!code-csharp", "[!code-vb", };
+
+ private static readonly string[] MarkdownExamples = new[] { "## Examples", "## Example" };
+
+ private static readonly string[] MarkdownHeaders = new[] { "[!NOTE]", "[!IMPORTANT]", "[!TIP]" };
+
+ public static SyntaxList GetXmlCommentLines(string[] commentLines)
+ {
+ var xmlTokens = commentLines
+ .Select(l => XmlTextLiteral(l, l))
+ .Zip(Enumerable.Repeat(XmlTextNewLine(Environment.NewLine), commentLines.Length - 1))
+ .SelectMany((pair) => new[] { pair.First, pair.Second });
+
+ var xmlText = XmlText(TokenList(xmlTokens));
+
+ return SingletonList(xmlText);
+ }
+
+ public static XmlNodeSyntax? GetSummary(DocsSummary summary)
+ {
+ var text = summary.ParsedText;
+
+ if (!text.IsDocsEmpty())
+ {
+ return XmlElement("summary", SingletonList(XmlText(XmlTextLiteral(text, text))));
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetSummary(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!api.Summary.IsDocsEmpty())
+ {
+ XmlTextSyntax content = GetTextAsCommentedTokens(api.Summary, leadingWhitespace);
+ XmlElementSyntax element = XmlSummaryElement(content);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static XmlNodeSyntax? GetRemarks(DocsRemarks remarks)
+ {
+ var text = remarks.ParsedText;
+
+ if (!text.IsDocsEmpty())
+ {
+ return XmlElement("remarks", SingletonList(XmlText(XmlTextLiteral(text, text))));
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetRemarks(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!api.Remarks.IsDocsEmpty())
+ {
+ return GetFormattedRemarks(api, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static XmlNodeSyntax? GetExample(DocsExample example)
+ {
+ if (!example.ParsedText.IsDocsEmpty())
+ {
+ var content = XmlText(example.ParsedText);
+ return XmlExampleElement(content);
+ }
+
+ return null;
+ }
+
+ public static IEnumerable GetExamples(IEnumerable examples)
+ {
+ return examples
+ .Where(e => !e.ParsedText?.IsDocsEmpty() ?? false)
+ .Select(GetExample)!;
+ }
+
+ public static XmlNodeSyntax? GetValue(string value)
+ {
+ if (!value.IsDocsEmpty())
+ {
+ return XmlValueElement(XmlText(value));
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetValue(DocsMember api, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!api.Value.IsDocsEmpty())
+ {
+ XmlTextSyntax contents = GetTextAsCommentedTokens(api.Value, leadingWhitespace);
+ XmlElementSyntax element = XmlValueElement(contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static XmlNodeSyntax? GetParameter(DocsParam parameter)
+ {
+ if (!parameter.ParsedText.IsDocsEmpty())
+ {
+ var content = XmlText(parameter.ParsedText);
+ return XmlParamElement(parameter.Name, content);
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetParameter(string name, string text, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!text.IsDocsEmpty())
+ {
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ XmlElementSyntax element = XmlParamElement(name, contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static IEnumerable GetParameters(IEnumerable parameters)
+ {
+ return parameters
+ .Where(param => !param.ParsedText.IsDocsEmpty())
+ .Select(GetParameter)!;
+ }
+
+ public static SyntaxTriviaList GetParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList parameters = new();
+ foreach (SyntaxTriviaList parameterTrivia in api.Params
+ .Where(param => !param.Value.IsDocsEmpty())
+ .Select(param => GetParameter(param.Name, param.Value, leadingWhitespace)))
+ {
+ parameters = parameters.AddRange(parameterTrivia);
+ }
+ return parameters;
+ }
+
+ public static XmlNodeSyntax? GetTypeParameter(DocsTypeParam param)
+ {
+ if (!param.ParsedText.IsDocsEmpty())
+ {
+ var attribute = new SyntaxList(XmlTextAttribute("name", param.Name));
+ var content = XmlText(param.ParsedText);
+
+ return GetXmlNode("typeparam", attribute, content);
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetTypeParam(string name, string text, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!text.IsDocsEmpty())
+ {
+ var attribute = new SyntaxList(XmlTextAttribute("name", name));
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ return GetXmlTrivia("typeparam", attribute, contents, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static IEnumerable GetTypeParameters(IEnumerable typeParams)
+ {
+ return typeParams
+ .Where(typeParam => !typeParam.ParsedText.IsDocsEmpty())
+ .Select(GetTypeParameter)!;
+ }
+
+ public static SyntaxTriviaList GetTypeParameters(DocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList typeParameters = new();
+ foreach (SyntaxTriviaList typeParameterTrivia in api.TypeParams
+ .Where(typeParam => !typeParam.Value.IsDocsEmpty())
+ .Select(typeParam => GetTypeParam(typeParam.Name, typeParam.Value, leadingWhitespace)))
+ {
+ typeParameters = typeParameters.AddRange(typeParameterTrivia);
+ }
+ return typeParameters;
+ }
+
+ public static XmlNodeSyntax? GetReturns(string returns)
+ {
+ if (!returns.IsDocsEmpty())
+ {
+ return XmlReturnsElement(XmlText(returns));
+ }
+
+ return null;
+ }
+
+ public static SyntaxTriviaList GetReturns(DocsMember api, SyntaxTriviaList leadingWhitespace)
+ {
+ // Also applies for when is empty because the method return type is void
+ if (!api.Returns.IsDocsEmpty())
+ {
+ XmlTextSyntax contents = GetTextAsCommentedTokens(api.Returns, leadingWhitespace);
+ XmlElementSyntax element = XmlReturnsElement(contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static XmlNodeSyntax GetException(DocsException exception)
+ {
+ var withoutPrefix = RemoveCrefPrefix(exception.Cref);
+ var crefType = TypeCref(ParseTypeName(withoutPrefix));
+
+ return XmlExceptionElement(crefType, XmlText(XmlTextLiteral(exception.Value, exception.Value)));
+ }
+
+ public static SyntaxTriviaList GetException(string cref, string text, SyntaxTriviaList leadingWhitespace)
+ {
+ if (!text.IsDocsEmpty())
+ {
+ cref = RemoveCrefPrefix(cref);
+ TypeCrefSyntax crefSyntax = TypeCref(ParseTypeName(cref));
+ XmlTextSyntax contents = GetTextAsCommentedTokens(text, leadingWhitespace);
+ XmlElementSyntax element = XmlExceptionElement(crefSyntax, contents);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ return new();
+ }
+
+ public static IEnumerable GetExceptions(List docsExceptions) =>
+ docsExceptions.Select(GetException);
+
+ public static SyntaxTriviaList GetExceptions(List docsExceptions, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList exceptions = new();
+ if (docsExceptions.Any())
+ {
+ foreach (SyntaxTriviaList exceptionsTrivia in docsExceptions.Select(
+ exception => GetException(exception.Cref, exception.Value, leadingWhitespace)))
+ {
+ exceptions = exceptions.AddRange(exceptionsTrivia);
+ }
+ }
+ return exceptions;
+ }
+
+ public static XmlNodeSyntax GetSeeAlso(string cref)
+ {
+ var withoutPrefix = RemoveCrefPrefix(cref);
+ var crefType = TypeCref(ParseTypeName(withoutPrefix));
+
+ return XmlSeeAlsoElement(crefType);
+ }
+
+ public static SyntaxTriviaList GetSeeAlso(string cref, SyntaxTriviaList leadingWhitespace)
+ {
+ cref = RemoveCrefPrefix(cref);
+ TypeCrefSyntax crefSyntax = TypeCref(ParseTypeName(cref));
+ XmlEmptyElementSyntax element = XmlSeeAlsoElement(crefSyntax);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ public static IEnumerable GetSeeAlsos(List docsSeeAlsoCrefs) =>
+ docsSeeAlsoCrefs.Select(GetSeeAlso);
+
+ public static SyntaxTriviaList GetSeeAlsos(List docsSeeAlsoCrefs, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList seealsos = new();
+ if (docsSeeAlsoCrefs.Any())
+ {
+ foreach (SyntaxTriviaList seealsoTrivia in docsSeeAlsoCrefs.Select(
+ s => GetSeeAlso(s, leadingWhitespace)))
+ {
+ seealsos = seealsos.AddRange(seealsoTrivia);
+ }
+ }
+ return seealsos;
+ }
+
+ public static XmlNodeSyntax GetAltMember(string cref)
+ {
+ var withoutPrefix = RemoveCrefPrefix(cref);
+ var crefType = MapDocIdGenericsToCrefGenerics(withoutPrefix);
+
+ return XmlEmptyElement(XmlName("altmember"), new(new[] { XmlTextAttribute("cref", crefType) }));
+ }
+
+ public static SyntaxTriviaList GetAltMember(string cref, SyntaxTriviaList leadingWhitespace)
+ {
+ cref = RemoveCrefPrefix(cref);
+ cref = MapDocIdGenericsToCrefGenerics(cref);
+ XmlAttributeSyntax attribute = XmlTextAttribute("cref", cref);
+ XmlEmptyElementSyntax emptyElement = XmlEmptyElement(XmlName(Identifier("altmember")), new SyntaxList(attribute));
+ return GetXmlTrivia(emptyElement, leadingWhitespace);
+ }
+
+ public static IEnumerable GetAltMembers(List docsAltMembers)
+ => docsAltMembers.Select(GetAltMember);
+
+ public static SyntaxTriviaList GetAltMembers(List docsAltMembers, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList altMembers = new();
+ if (docsAltMembers.Any())
+ {
+ foreach (SyntaxTriviaList altMemberTrivia in docsAltMembers.Select(
+ s => GetAltMember(s, leadingWhitespace)))
+ {
+ altMembers = altMembers.AddRange(altMemberTrivia);
+ }
+ }
+ return altMembers;
+ }
+
+ public static XmlNodeSyntax GetRelated(DocsRelated related)
+ {
+ var attributes = new[]
+ {
+ XmlTextAttribute("type", related.ArticleType),
+ XmlTextAttribute("href", related.Href)
+ };
+
+ var start = XmlElementStartTag(XmlName("related"), new(attributes));
+ var end = XmlElementEndTag(XmlName("related"));
+
+ return XmlElement(start, new(XmlText(XmlTextLiteral(related.Value))), end);
+ }
+
+ public static SyntaxTriviaList GetRelated(string articleType, string href, string value, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxList attributes = new();
+
+ attributes = attributes.Add(XmlTextAttribute("type", articleType));
+ attributes = attributes.Add(XmlTextAttribute("href", href));
+
+ XmlTextSyntax contents = GetTextAsCommentedTokens(value, leadingWhitespace);
+ return GetXmlTrivia("related", attributes, contents, leadingWhitespace);
+ }
+
+ public static IEnumerable GetRelateds(List docsRelateds) =>
+ docsRelateds.Select(GetRelated);
+
+ public static SyntaxTriviaList GetRelateds(List docsRelateds, SyntaxTriviaList leadingWhitespace)
+ {
+ SyntaxTriviaList relateds = new();
+ if (docsRelateds.Any())
+ {
+ foreach (SyntaxTriviaList relatedsTrivia in docsRelateds.Select(
+ s => GetRelated(s.ArticleType, s.Href, s.Value, leadingWhitespace)))
+ {
+ relateds = relateds.AddRange(relatedsTrivia);
+ }
+ }
+ return relateds;
+ }
+
+ private static XmlTextSyntax GetTextAsCommentedTokens(string text, SyntaxTriviaList leadingWhitespace, bool wrapWithNewLines = false)
+ {
+ text = CleanCrefs(text);
+
+ // collapse newlines to a single one
+ string whitespace = Regex.Replace(leadingWhitespace.ToFullString(), @"(\r?\n)+", "");
+ SyntaxToken whitespaceToken = XmlTextNewLine(Environment.NewLine + whitespace);
+
+ SyntaxTrivia leadingTrivia = SyntaxTrivia(SyntaxKind.DocumentationCommentExteriorTrivia, string.Empty);
+ SyntaxTriviaList leading = SyntaxTriviaList.Create(leadingTrivia);
+
+ string[] lines = text.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ var tokens = new List();
+
+ if (wrapWithNewLines)
+ {
+ tokens.Add(whitespaceToken);
+ }
+
+ for (int lineNumber = 0; lineNumber < lines.Length; lineNumber++)
+ {
+ string line = lines[lineNumber];
+
+ SyntaxToken token = XmlTextLiteral(leading, line, line, default);
+ tokens.Add(token);
+
+ if (lines.Length > 1 && lineNumber < lines.Length - 1)
+ {
+ tokens.Add(whitespaceToken);
+ }
+ }
+
+ if (wrapWithNewLines)
+ {
+ tokens.Add(whitespaceToken);
+ }
+
+ XmlTextSyntax xmlText = XmlText(tokens.ToArray());
+ return xmlText;
+ }
+
+ private static SyntaxTriviaList GetXmlTrivia(XmlNodeSyntax node, SyntaxTriviaList leadingWhitespace)
+ {
+ DocumentationCommentTriviaSyntax docComment = DocumentationComment(node);
+ SyntaxTrivia docCommentTrivia = Trivia(docComment);
+
+ return leadingWhitespace
+ .Add(docCommentTrivia)
+ .Add(CarriageReturnLineFeed);
+ }
+
+ private static XmlNodeSyntax GetXmlNode(string name, SyntaxList attributes, XmlTextSyntax content)
+ {
+ var start = XmlElementStartTag(
+ Token(SyntaxKind.LessThanToken),
+ XmlName(Identifier(name)),
+ attributes,
+ Token(SyntaxKind.GreaterThanToken));
+
+ var end = XmlElementEndTag(
+ Token(SyntaxKind.LessThanSlashToken),
+ XmlName(Identifier(name)),
+ Token(SyntaxKind.GreaterThanToken));
+
+ return XmlElement(start, new(content), end);
+ }
+
+ // Generates a custom SyntaxTrivia object containing a triple slashed xml element with optional attributes.
+ // Looks like below (excluding square brackets):
+ // [ /// text]
+ private static SyntaxTriviaList GetXmlTrivia(string name, SyntaxList attributes, XmlTextSyntax contents, SyntaxTriviaList leadingWhitespace)
+ {
+ XmlElementStartTagSyntax start = XmlElementStartTag(
+ Token(SyntaxKind.LessThanToken),
+ XmlName(Identifier(name)),
+ attributes,
+ Token(SyntaxKind.GreaterThanToken));
+
+ XmlElementEndTagSyntax end = XmlElementEndTag(
+ Token(SyntaxKind.LessThanSlashToken),
+ XmlName(Identifier(name)),
+ Token(SyntaxKind.GreaterThanToken));
+
+ XmlElementSyntax element = XmlElement(start, new SyntaxList(contents), end);
+ return GetXmlTrivia(element, leadingWhitespace);
+ }
+
+ private static string WrapInRemarks(string acum)
+ {
+ string wrapped = Environment.NewLine + "" + Environment.NewLine;
+ return wrapped;
+ }
+
+ private static string WrapCodeIncludes(string[] splitted, ref int n)
+ {
+ string acum = string.Empty;
+ while (n < splitted.Length && splitted[n].ContainsStrings(MarkdownCodeIncludes))
+ {
+ acum += Environment.NewLine + splitted[n];
+ if ((n + 1) < splitted.Length && splitted[n + 1].ContainsStrings(MarkdownCodeIncludes))
+ {
+ n++;
+ }
+ else
+ {
+ break;
+ }
+ }
+ return WrapInRemarks(acum);
+ }
+
+ private static SyntaxTriviaList GetFormattedRemarks(IDocsAPI api, SyntaxTriviaList leadingWhitespace)
+ {
+
+ string remarks = RemoveUnnecessaryMarkdown(api.Remarks);
+ string example = string.Empty;
+
+ XmlNodeSyntax contents;
+ if (remarks.ContainsStrings(MarkdownUnconvertableStrings))
+ {
+ contents = GetTextAsFormatCData(remarks, leadingWhitespace);
+ }
+ else
+ {
+ string[] splitted = remarks.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
+ string updatedRemarks = string.Empty;
+ for (int n = 0; n < splitted.Length; n++)
+ {
+ string acum;
+ string line = splitted[n];
+ if (line.ContainsStrings(MarkdownHeaders))
+ {
+ acum = line;
+ n++;
+ while (n < splitted.Length && splitted[n].StartsWith(">"))
+ {
+ acum += Environment.NewLine + splitted[n];
+ if ((n + 1) < splitted.Length && splitted[n + 1].StartsWith(">"))
+ {
+ n++;
+ }
+ else
+ {
+ break;
+ }
+ }
+ updatedRemarks += WrapInRemarks(acum);
+ }
+ else if (line.ContainsStrings(MarkdownCodeIncludes))
+ {
+ updatedRemarks += WrapCodeIncludes(splitted, ref n);
+ }
+ // When an example is found, everything after the header is considered part of that section
+ else if (line.Contains("## Example"))
+ {
+ n++;
+ while (n < splitted.Length)
+ {
+ line = splitted[n];
+ if (line.ContainsStrings(MarkdownCodeIncludes))
+ {
+ example += WrapCodeIncludes(splitted, ref n);
+ }
+ else
+ {
+ example += Environment.NewLine + line;
+ }
+ n++;
+ }
+ }
+ else
+ {
+ updatedRemarks += ReplaceMarkdownWithXmlElements(Environment.NewLine + line, api.Params, api.TypeParams);
+ }
+ }
+
+ contents = GetTextAsCommentedTokens(updatedRemarks, leadingWhitespace);
+ }
+
+ XmlElementSyntax remarksXml = XmlRemarksElement(contents);
+ SyntaxTriviaList result = GetXmlTrivia(remarksXml, leadingWhitespace);
+
+ if (!string.IsNullOrWhiteSpace(example))
+ {
+ SyntaxTriviaList exampleTriviaList = GetFormattedExamples(api, example, leadingWhitespace);
+ result = result.AddRange(exampleTriviaList);
+ }
+
+ return result;
+ }
+
+ private static SyntaxTriviaList GetFormattedExamples(IDocsAPI api, string example, SyntaxTriviaList leadingWhitespace)
+ {
+ example = ReplaceMarkdownWithXmlElements(example, api.Params, api.TypeParams);
+ XmlNodeSyntax exampleContents = GetTextAsCommentedTokens(example, leadingWhitespace);
+ XmlElementSyntax exampleXml = XmlExampleElement(exampleContents);
+ SyntaxTriviaList exampleTriviaList = GetXmlTrivia(exampleXml, leadingWhitespace);
+ return exampleTriviaList;
+ }
+
+ private static XmlNodeSyntax GetTextAsFormatCData(string text, SyntaxTriviaList leadingWhitespace)
+ {
+ XmlTextSyntax remarks = GetTextAsCommentedTokens(text, leadingWhitespace, wrapWithNewLines: true);
+
+ XmlNameSyntax formatName = XmlName("format");
+ XmlAttributeSyntax formatAttribute = XmlTextAttribute("type", "text/markdown");
+ var formatAttributes = new SyntaxList(formatAttribute);
+
+ var formatStart = XmlElementStartTag(formatName, formatAttributes);
+ var formatEnd = XmlElementEndTag(formatName);
+
+ XmlCDataSectionSyntax cdata = XmlCDataSection(remarks.TextTokens);
+ var cdataList = new SyntaxList(cdata);
+
+ XmlElementSyntax contents = XmlElement(formatStart, cdataList, formatEnd);
+
+ return contents;
+ }
+
+ private static string RemoveUnnecessaryMarkdown(string text)
+ {
+ text = Regex.Replace(text, @"", "");
+ text = Regex.Replace(text, @"##[ ]?Remarks(\r?\n)*[\t ]*", "");
+ return text;
+ }
+
+ private static string ReplaceMarkdownWithXmlElements(string text, List docsParams, List docsTypeParams)
+ {
+ text = CleanXrefs(text);
+
+ // commonly used url entities
+ text = Regex.Replace(text, @"%23", "#");
+ text = Regex.Replace(text, @"%28", "(");
+ text = Regex.Replace(text, @"%29", ")");
+ text = Regex.Replace(text, @"%2C", ",");
+
+ // hyperlinks
+ text = Regex.Replace(text, RegexMarkdownLinkPattern, RegexHtmlLinkReplacement);
+
+ // bold
+ text = Regex.Replace(text, RegexMarkdownBoldPattern, RegexXmlBoldReplacement);
+
+ // code snippet
+ text = Regex.Replace(text, RegexMarkdownCodeStartPattern, RegexXmlCodeStartReplacement);
+ text = Regex.Replace(text, RegexMarkdownCodeEndPattern, RegexXmlCodeEndReplacement);
+
+ // langwords|parameters|typeparams and other type references within markdown backticks
+ MatchCollection collection = Regex.Matches(text, @"(?`(?[a-zA-Z0-9_]+(?\<(?[a-zA-Z0-9_,]+)\>){0,1})`)");
+ foreach (Match match in collection)
+ {
+ string backtickContent = match.Groups["backtickContent"].Value;
+ string backtickedApi = match.Groups["backtickedApi"].Value;
+ Group genericType = match.Groups["genericType"];
+ Group typeParam = match.Groups["typeParam"];
+
+ if (genericType.Success && typeParam.Success)
+ {
+ backtickedApi = backtickedApi.Replace(genericType.Value, $"{{{typeParam.Value}}}");
+ }
+
+ if (ReservedKeywords.Any(x => x == backtickedApi))
+ {
+ text = Regex.Replace(text, $"{backtickContent}", $"");
+ }
+ else if (docsParams.Any(x => x.Name == backtickedApi))
+ {
+ text = Regex.Replace(text, $"{backtickContent}", $"");
+ }
+ else if (docsTypeParams.Any(x => x.Name == backtickedApi))
+ {
+ text = Regex.Replace(text, $"{backtickContent}", $"");
+ }
+ else
+ {
+ text = Regex.Replace(text, $"{backtickContent}", $"");
+ }
+ }
+
+ return text;
+ }
+
+ // Removes the one letter prefix and the following colon, if found, from a cref.
+ private static string RemoveCrefPrefix(string cref)
+ {
+ if (cref.Length > 2 && cref[1] == ':')
+ {
+ return cref[2..];
+ }
+ return cref;
+ }
+
+ private static string ReplacePrimitives(string text)
+ {
+ foreach ((string key, string value) in PrimitiveTypes)
+ {
+ text = Regex.Replace(text, key, value);
+ }
+ return text;
+ }
+
+ private static string ReplaceDocId(Match m)
+ {
+ string docId = m.Groups["docId"].Value;
+ string? prefix = m.Groups["prefix"].Value == "O:" ? "O:" : null;
+ docId = ReplacePrimitives(docId);
+ docId = System.Net.WebUtility.UrlDecode(docId);
+
+ // Strip '*' character from the tail end of DocId names
+ if (docId.EndsWith('*'))
+ {
+ prefix = "O:";
+ docId = docId[..^1];
+ }
+
+ return prefix + MapDocIdGenericsToCrefGenerics(docId);
+ }
+
+ private static string MapDocIdGenericsToCrefGenerics(string docId)
+ {
+ // Map DocId generic parameters to Xml Doc generic parameters
+ // need to support both single and double backtick syntax
+ const string GenericParameterPattern = @"`{1,2}([\d+])";
+ int genericParameterArity = 0;
+ return Regex.Replace(docId, GenericParameterPattern, MapDocIdGenericParameterToXmlDocGenericParameter);
+
+ string MapDocIdGenericParameterToXmlDocGenericParameter(Match match)
+ {
+ int index = int.Parse(match.Groups[1].Value);
+
+ if (genericParameterArity == 0)
+ {
+ // this is the first match that declares the generic parameter arity of the method
+ // e.g. GenericMethod``3 ---> GenericMethod{T1,T2,T3}(...);
+ Debug.Assert(index > 0);
+ genericParameterArity = index;
+ return WrapInCurlyBrackets(string.Join(",", Enumerable.Range(0, index).Select(CreateGenericParameterName)));
+ }
+
+ // Subsequent matches are references to generic parameters in the method signature,
+ // e.g. GenericMethod{T1,T2,T3}(..., List{``1} parameter, ...); ---> List{T2} parameter
+ return CreateGenericParameterName(index);
+
+ // NB this naming scheme does not map to the exact generic parameter names,
+ // however this is still accepted by intellisense and backporters can rename
+ // manually with the help of tooling.
+ string CreateGenericParameterName(int index)
+ => genericParameterArity == 1 ? "T" : $"T{index + 1}";
+
+ static string WrapInCurlyBrackets(string input) => $"{{{input}}}";
+ }
+ }
+
+ private static string CrefEvaluator(Match m)
+ {
+ string docId = ReplaceDocId(m);
+ return "cref=\"" + docId + "\"";
+ }
+
+ private static string CleanCrefs(string text)
+ {
+ text = Regex.Replace(text, RegexXmlCrefPattern, CrefEvaluator);
+ return text;
+ }
+
+ private static string XrefEvaluator(Match m)
+ {
+ string docId = ReplaceDocId(m);
+ return "";
+ }
+
+ private static string CleanXrefs(string text)
+ {
+ text = Regex.Replace(text, RegexMarkdownXrefPattern, XrefEvaluator);
+ return text;
+ }
+ }
+}
diff --git a/Libraries/XmlHelper.cs b/Libraries/XmlHelper.cs
index b1e40fc..fe40eae 100644
--- a/Libraries/XmlHelper.cs
+++ b/Libraries/XmlHelper.cs
@@ -9,7 +9,8 @@ namespace Libraries
{
internal class XmlHelper
{
- private static readonly Dictionary _replaceableNormalElementPatterns = new Dictionary {
+ private static readonly Dictionary _replaceableNormalElementPatterns = new()
+ {
{ "null", ""},
{ "true", ""},
{ "false", ""},
@@ -31,7 +32,8 @@ internal class XmlHelper
{ ">", " />" }
};
- private static readonly Dictionary _replaceableMarkdownPatterns = new Dictionary {
+ private static readonly Dictionary _replaceableMarkdownPatterns = new()
+ {
{ "", "`null`" },
{ "", "`null`" },
{ "", "`true`" },
@@ -72,13 +74,15 @@ internal class XmlHelper
{ "", "" }
};
- private static readonly Dictionary _replaceableExceptionPatterns = new Dictionary{
+ private static readonly Dictionary _replaceableExceptionPatterns = new()
+ {
{ "", "\r\n" },
{ "", "" }
};
- private static readonly Dictionary _replaceableMarkdownRegexPatterns = new Dictionary {
+ private static readonly Dictionary _replaceableMarkdownRegexPatterns = new()
+ {
{ @"\", @"`${paramrefContents}`" },
{ @"\", @"seealsoContents" },
};
@@ -153,7 +157,7 @@ public static void SaveFormattedAsMarkdown(XElement element, string newValue, bo
// Empty value because SaveChildElement will add a child to the parent, not replace it
element.Value = string.Empty;
- XElement xeFormat = new XElement("format");
+ XElement xeFormat = new("format");
string updatedValue = SubstituteRemarksRegexPatterns(newValue);
updatedValue = ReplaceMarkdownPatterns(updatedValue).Trim();
@@ -305,7 +309,7 @@ private static string SubstituteRegexPatterns(string value, Dictionary pattern in replaceableRegexPatterns)
{
- Regex regex = new Regex(pattern.Key);
+ Regex regex = new(pattern.Key);
if (regex.IsMatch(value))
{
value = regex.Replace(value, pattern.Value);
diff --git a/Packages.props b/Packages.props
new file mode 100644
index 0000000..83413fe
--- /dev/null
+++ b/Packages.props
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/Program/DocsPortingTool.csproj b/Program/DocsPortingTool.csproj
index 67aff36..8a41b1c 100644
--- a/Program/DocsPortingTool.csproj
+++ b/Program/DocsPortingTool.csproj
@@ -14,10 +14,10 @@
-
-
-
-
+
+
+
+
diff --git a/Tests/DocsApi/DocsApiReferenceTests.cs b/Tests/DocsApi/DocsApiReferenceTests.cs
new file mode 100644
index 0000000..d970800
--- /dev/null
+++ b/Tests/DocsApi/DocsApiReferenceTests.cs
@@ -0,0 +1,88 @@
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsApiReferenceTests
+ {
+ [Theory]
+ [InlineData("System.Boolean", "bool")]
+ [InlineData("System.Byte", "byte")]
+ [InlineData("System.Char", "char")]
+ [InlineData("System.Decimal", "decimal")]
+ [InlineData("System.Double", "double")]
+ [InlineData("System.Int16", "short")]
+ [InlineData("System.Int32", "int")]
+ [InlineData("System.Int64", "long")]
+ [InlineData("System.Object", "object")]
+ [InlineData("System.SByte", "sbyte")]
+ [InlineData("System.Single", "float")]
+ [InlineData("System.String", "string")]
+ [InlineData("System.UInt16", "ushort")]
+ [InlineData("System.UInt32", "uint")]
+ [InlineData("System.UInt64", "ulong")]
+ [InlineData("System.Void", "void")]
+ [InlineData("System.Int32.ToString(System.String)", "int.ToString(string)")]
+ public void ReturnsShorthandPrimitives(string apiReference, string expected)
+ {
+ var reference = new DocsApiReference(apiReference);
+ Assert.Equal(expected, reference.Api);
+ }
+
+ [Theory]
+ [InlineData("T:System.Int32", "int")]
+ [InlineData("M:System.Int32.ToString()", "int.ToString()")]
+ [InlineData("O:System.Int32.ToString(System.String)", "int.ToString(string)")]
+ public void RemovesPrefixFromApi(string apiReference, string expected)
+ {
+ var reference = new DocsApiReference(apiReference);
+ Assert.Equal(expected, reference.Api);
+ }
+
+ [Theory]
+ [InlineData("System.Int32", "int")]
+ [InlineData("System.Int32.ToString()", "int.ToString()")]
+ [InlineData("System.Int32.ToString(System.String)", "int.ToString(string)")]
+ [InlineData("T:System.Int32", "T:int")]
+ [InlineData("M:System.Int32.ToString()", "M:int.ToString()")]
+ [InlineData("O:System.Int32.ToString(System.String)", "O:int.ToString(string)")]
+ public void OverridesToStringWithPrefixAndApi(string apiReference, string expected)
+ {
+ var reference = new DocsApiReference(apiReference);
+ Assert.Equal(expected, reference.ToString());
+ }
+
+ [Theory]
+ [InlineData("MyNamespace.MyGenericType.Select`1", "MyNamespace.MyGenericType.Select{T}")]
+ [InlineData("MyNamespace.MyGenericType.Select`2", "MyNamespace.MyGenericType.Select{T1,T2}")]
+ [InlineData("MyNamespace.MyGenericType.Select``2(MyNamespace.MyGenericType{``0},System.Func{``0,``1})", "MyNamespace.MyGenericType.Select{T1,T2}(MyNamespace.MyGenericType{T1},System.Func{T1,T2})")]
+ [InlineData("MyNamespace.MyGenericType.Select%60%602%28MyNamespace.MyGenericType%7B%60%600%7D%2CSystem.Func%7B%60%600%2C%60%601%7D%29", "MyNamespace.MyGenericType.Select{T1,T2}(MyNamespace.MyGenericType{T1},System.Func{T1,T2})")]
+ public void ParsesGenerics(string apiReference, string expected)
+ {
+ var reference = new DocsApiReference(apiReference);
+ Assert.Equal(expected, reference.Api);
+ }
+
+ [Theory]
+ [InlineData("System.Int32", false)]
+ [InlineData("T:System.Int32", false)]
+ [InlineData("M:System.Int32.ToString()", false)]
+ [InlineData("O:System.Int32.ToString(System.String)", true)]
+ public void IdentifiesOverloadReferences(string apiReference, bool expected)
+ {
+ var reference = new DocsApiReference(apiReference);
+ Assert.Equal(expected, reference.IsOverload);
+ }
+
+ [Theory]
+ [InlineData(@"Accessibility: ", @"Accessibility: ")]
+ [InlineData(@"The `MyGenericType` type contains the nested class .", @"The `MyGenericType` type contains the nested class .")]
+ [InlineData(@"SyndicationCategory: ", @"SyndicationCategory: ")]
+ [InlineData(@"Label: ", @"Label: ")]
+ [InlineData(@"==: ", @"==: ")]
+ public void ReplacesMarkdownXrefsWithSeeCrefs(string markdown, string expected)
+ {
+ var replaced = DocsApiReference.ReplaceMarkdownXrefWithSeeCref(markdown);
+ Assert.Equal(expected, replaced);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsAssemblyInfoTests.cs b/Tests/DocsApi/DocsAssemblyInfoTests.cs
new file mode 100644
index 0000000..a1d7b5d
--- /dev/null
+++ b/Tests/DocsApi/DocsAssemblyInfoTests.cs
@@ -0,0 +1,39 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsAssemblyInfoTests
+ {
+ [Fact]
+ public void ExtractsAssemblyName()
+ {
+ var assembly = new DocsAssemblyInfo(XElement.Parse(@"
+
+ MyAssembly
+ "
+ ));
+
+ Assert.Equal("MyAssembly", assembly.AssemblyName);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+ 4.0.0.0
+ ",
+ new string[] { "4.0.0.0" })]
+ [InlineData(@"
+
+ 4.0.0.0
+ 5.0.0.0
+ ",
+ new string[] { "4.0.0.0", "5.0.0.0" })]
+ public void ExtractsOneAssemblyVersion(string xml, string[] expected)
+ {
+ var assembly = new DocsAssemblyInfo(XElement.Parse(xml));
+
+ Assert.Equal(expected, assembly.AssemblyVersions);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsAttributeTests.cs b/Tests/DocsApi/DocsAttributeTests.cs
new file mode 100644
index 0000000..cced809
--- /dev/null
+++ b/Tests/DocsApi/DocsAttributeTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsAttributeTests
+ {
+ [Theory]
+ [InlineData(
+ @"",
+ @"netframework-4.0")]
+ public void ExtractsFrameworkAlternate(string xml, string expected)
+ {
+ var attribute = new DocsAttribute(XElement.Parse(xml));
+ Assert.Equal(expected, attribute.FrameworkAlternate);
+ }
+
+ [Theory]
+ [InlineData("C#", @"[System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")]")]
+ [InlineData("F#", @"[]")]
+ public void ExtractsAttributeNameByLanguage(string language, string expected)
+ {
+ var attribute = new DocsAttribute(XElement.Parse(@"
+
+ [System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")]
+ [<System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")>]
+
+ "));
+
+ var actual = attribute.GetAttributeName(language);
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+ [System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")]
+ [<System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")>]
+ ",
+ @"[System.Runtime.TargetedPatchingOptOut(""Performance critical to inline this type of method across NGen image boundaries"")]")]
+ public void ExtractsAttributeNameForCsharpByDefault(string xml, string expected)
+ {
+ var attribute = new DocsAttribute(XElement.Parse(xml));
+ var actual = attribute.GetAttributeName("C#");
+
+ Assert.Equal(expected, actual);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsExampleTests.cs b/Tests/DocsApi/DocsExampleTests.cs
new file mode 100644
index 0000000..238971d
--- /dev/null
+++ b/Tests/DocsApi/DocsExampleTests.cs
@@ -0,0 +1,206 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsExampleTests
+ {
+ [Theory]
+ [InlineData(
+ @"Example.",
+ @"Example.")]
+ [InlineData(
+ @"Example referencing .",
+ @"Example referencing .")]
+ [InlineData(
+ @"
+ Multiline
+ Example
+ Referencing
+ .
+ ",
+ @"
+ Multiline
+ Example
+ Referencing
+ .
+ ")]
+ public void GetsRawText(string xml, string expected)
+ {
+ var example = new DocsExample(XElement.Parse(xml));
+ Assert.Equal(expected, example.RawText);
+ }
+
+ [Theory]
+ [InlineData(
+ @"Example.",
+ @"Example.")]
+ [InlineData(
+ @"Example referencing .",
+ @"Example referencing .")]
+ [InlineData(
+ @"
+ Multiline
+
+ Example
+
+ Referencing
+
+
+
+ With Blank Lines.
+ ",
+ @"Multiline
+Example
+Referencing
+
+With Blank Lines.")]
+ [InlineData(
+ @"
+
+ ",
+ @"Markdown example")]
+ public void GetsParsedText(string xml, string expected)
+ {
+ var example = new DocsExample(XElement.Parse(xml));
+ Assert.Equal(expected, example.ParsedText);
+ }
+
+ [Theory]
+ [InlineData( // [!INCLUDE
+ @"",
+ @"")]
+ [InlineData( // [!NOTE
+ @"",
+ @"")]
+ [InlineData( // [!IMPORTANT
+ @"",
+ @"")]
+ [InlineData( // [!TIP
+ @"",
+ @"")]
+ [InlineData( // [!code-cpp
+ @"",
+ @"")]
+ [InlineData( // [!code-csharp
+ @"",
+ @"")]
+ [InlineData( // [!code-vb
+ @"",
+ @"")]
+ public void RetainsMarkdownFormatForUnparseableContent(string xml, string expected)
+ {
+ var example = new DocsExample(XElement.Parse(xml));
+ Assert.Equal(expected, example.ParsedText);
+ }
+
+ [Theory]
+ [InlineData( // [!INCLUDE -- Without CDATA
+ @"Has an inline include. [!INCLUDE[include-file](~/includes/include-file.md)]",
+ @"Has an inline include. [!INCLUDE[include-file](~/includes/include-file.md)]")]
+ [InlineData( // [!INCLUDE -- With CDATA
+ @"",
+ @"")]
+ [InlineData( // [!INCLUDE -- With CDATA and newlines
+ @"",
+ @"")]
+ public void RetainsMarkdownStructure(string xml, string expected)
+ {
+ var example = new DocsExample(XElement.Parse(xml));
+ Assert.Equal(expected, example.ParsedText);
+ }
+
+ [Fact]
+ public void ReplacesMarkdownCodeSnippetsWithTags()
+ {
+ var xml = @"";
+
+ var expected = @"Here's an example:
+
+Console.WriteLine(""Hello World!"");
+
";
+
+ var example = new DocsExample(XElement.Parse(xml));
+ Assert.Equal(expected, example.ParsedText);
+ }
+
+ [Fact]
+ public void GetsNodes()
+ {
+ var xml = @"Example referencing a .";
+ var example = new DocsExample(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Example referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), example.RawNodes.ToArray().Select(x => x.ToString()));
+ }
+
+ [Fact]
+ public void CanIncludeSeeElements()
+ {
+ var xml = @"";
+ var example = new DocsExample(XElement.Parse(xml));
+ var see = example.RawNodes.Single();
+
+ Assert.Equal(XmlNodeType.Element, see.NodeType);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeElements()
+ {
+ var xml = @"";
+ var example = new DocsExample(XElement.Parse(xml));
+ var see = example.RawNodes.Single();
+
+ Assert.Equal("see", ((XElement)see).Name);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeCrefValues()
+ {
+ var xml = @"";
+ var example = new DocsExample(XElement.Parse(xml));
+ var see = example.RawNodes.Single();
+
+ Assert.Equal("T:System.Type", ((XElement)see).Attribute("cref").Value);
+ }
+
+ [Fact]
+ public void ParsesNodes()
+ {
+ var xml = @"Example referencing a .";
+ var example = new DocsExample(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Example referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), example.ParsedNodes.ToArray());
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsExceptionTests.cs b/Tests/DocsApi/DocsExceptionTests.cs
new file mode 100644
index 0000000..64fb11a
--- /dev/null
+++ b/Tests/DocsApi/DocsExceptionTests.cs
@@ -0,0 +1,49 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsExceptionTests
+ {
+ [Theory]
+ [InlineData(
+ @"
+ If a null reference is returned from ",
+ @"T:System.InvalidOperationException")]
+ public void ExtractsCref(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var exception = new DocsException(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, exception.Cref);
+ }
+
+ [Theory]
+ [InlineData(
+ @"
+ If a null reference is returned from ",
+ @"If a null reference is returned from ")]
+ [InlineData(
+ @"This is the IndexOutOfRangeException thrown by MyVoidMethod.
+
+-or-
+
+This is the second case.
+
+Empty newlines should be respected.",
+ @"This is the IndexOutOfRangeException thrown by MyVoidMethod.
+
+-or-
+
+This is the second case.
+
+Empty newlines should be respected.")]
+ public void ExtractsValueInPlainText(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var exception = new DocsException(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, exception.Value);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsParamTests.cs b/Tests/DocsApi/DocsParamTests.cs
new file mode 100644
index 0000000..c1e4a47
--- /dev/null
+++ b/Tests/DocsApi/DocsParamTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsParamTests
+ {
+ [Theory]
+ [InlineData(
+ @"The object to animate.",
+ @"image")]
+ public void ExtractsName(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var param = new DocsParam(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, param.Name);
+ }
+
+ [Theory]
+ [InlineData(
+ @"The object to animate.",
+ @"The object to animate.")]
+ [InlineData(
+ @"The object to be added to the end of the . The value can be for reference types.",
+ @"The object to be added to the end of the . The value can be for reference types.")]
+ public void ExtractsValueAsPlainText(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var param = new DocsParam(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, param.Value);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsParameterTests.cs b/Tests/DocsApi/DocsParameterTests.cs
new file mode 100644
index 0000000..f1366fb
--- /dev/null
+++ b/Tests/DocsApi/DocsParameterTests.cs
@@ -0,0 +1,31 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsParameterTests
+ {
+ [Theory]
+ [InlineData(
+ @"",
+ @"image")]
+ public void ExtractsName(string xml, string expected)
+ {
+ var parameter = new DocsParameter(XElement.Parse(xml));
+ Assert.Equal(expected, parameter.Name);
+ }
+
+ [Theory]
+ [InlineData(
+ @"",
+ @"System.Drawing.Image")]
+ [InlineData(
+ @"",
+ @"System.Collections.Generic.IEnumerable")]
+ public void ExtractsType(string xml, string expected)
+ {
+ var parameter = new DocsParameter(XElement.Parse(xml));
+ Assert.Equal(expected, parameter.Type);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsRelatedTests.cs b/Tests/DocsApi/DocsRelatedTests.cs
new file mode 100644
index 0000000..6054518
--- /dev/null
+++ b/Tests/DocsApi/DocsRelatedTests.cs
@@ -0,0 +1,48 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsRelatedTests
+ {
+ [Fact]
+ public void ExtractsArticleType()
+ {
+ var parent = new TestDocsApi();
+ var related = new DocsRelated(parent, XElement.Parse(@"
+ Iterators (C#)
+ "));
+
+ Assert.Equal("Article", related.ArticleType);
+ }
+
+ [Fact]
+ public void ExtractsHref()
+ {
+ var parent = new TestDocsApi();
+ var related = new DocsRelated(parent, XElement.Parse(@"
+ Iterators (C#)
+ "));
+
+ Assert.Equal("/dotnet/csharp/programming-guide/concepts/iterators", related.Href);
+ }
+
+ [Theory]
+ [InlineData(
+ @"Iterators (C#)",
+ @"Iterators (C#)")]
+ [InlineData(
+ @"<compilers> Element",
+ @"<compilers> Element")]
+ [InlineData(
+ @"Memory<T> and Span<T> usage guidelines",
+ @"Memory<T> and Span<T> usage guidelines")]
+ public void ExtractsValueAsPlainText(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var related = new DocsRelated(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, related.Value);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsRemarksTests.cs b/Tests/DocsApi/DocsRemarksTests.cs
new file mode 100644
index 0000000..487d0e7
--- /dev/null
+++ b/Tests/DocsApi/DocsRemarksTests.cs
@@ -0,0 +1,394 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsRemarksTests
+ {
+ [Theory]
+ [InlineData(
+ @"Remarks.",
+ @"Remarks.")]
+ [InlineData(
+ @"Remarks referencing .",
+ @"Remarks referencing .")]
+ [InlineData(
+ @"
+ Multiline
+ Remarks
+ Referencing
+ .
+ ",
+ @"
+ Multiline
+ Remarks
+ Referencing
+ .
+ ")]
+ [InlineData(
+ @"
+ .
+
+There are also a `true` and a `null`.
+
+ ]]>
+ ",
+ @".
+
+There are also a `true` and a `null`.
+
+ ]]>")]
+ public void GetsRawText(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.RawText);
+ }
+
+ [Theory]
+ [InlineData(
+ @"Remarks.",
+ @"Remarks.")]
+ [InlineData(
+ @"Remarks referencing .",
+ @"Remarks referencing .")]
+ [InlineData(
+ @"
+ Multiline
+
+ Remarks
+
+ Referencing
+
+
+
+ With Blank Lines.
+ ",
+ @"Multiline
+Remarks
+Referencing
+
+With Blank Lines.")]
+ [InlineData(
+ @" This is the MyTypeParamMethod typeparam T.
+
+ .
+
+There are also a `true` and a `null`.
+
+ ]]>
+ ",
+ @"This is a reference to the typeparam .
+This is a reference to the parameter .
+Mentions the and an .
+There are also a and a .")]
+ public void GetsParsedText(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(
+ @"
+
+ ",
+ @"Markdown remarks")]
+ [InlineData(
+ @"
+
+ ",
+ @"Markdown remarks")]
+ public void RemovesRemarksHeader(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData( // [!INCLUDE
+ @"",
+ @"")]
+ [InlineData( // [!NOTE
+ @"",
+ @"")]
+ [InlineData( // [!IMPORTANT
+ @"",
+ @"")]
+ [InlineData( // [!TIP
+ @"",
+ @"")]
+ [InlineData( // [!code-cpp
+ @"",
+ @"")]
+ [InlineData( // [!code-csharp
+ @"",
+ @"")]
+ [InlineData( // [!code-vb
+ @"",
+ @"")]
+ public void RetainsMarkdownFormatForUnparseableContent(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData( // [!INCLUDE -- Without CDATA
+ @"Has an inline include. [!INCLUDE[include-file](~/includes/include-file.md)]",
+ @"Has an inline include. [!INCLUDE[include-file](~/includes/include-file.md)]")]
+ [InlineData( // [!INCLUDE -- With CDATA
+ @"",
+ @"")]
+ [InlineData( // [!INCLUDE -- With CDATA and newlines
+ @"",
+ @"")]
+ public void RetainsMarkdownStructure(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(
+ @"",
+ @"")]
+ public void RetainsMarkdownStructureButRemovesHeader(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Fact]
+ public void ReplacesMarkdownXrefWithTags()
+ {
+ var xml = @".
+ ]]>";
+
+ var expected = @"See .";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Fact]
+ public void ReplacesMarkdownLinksWithTags()
+ {
+ var xml = @"";
+
+ var expected = @"See the web.";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(@"Use `async` methods.", @"Use methods.")]
+ [InlineData(@"The `T` generic type parameter must be a struct.", @"The generic type parameter must be a struct.")]
+ [InlineData(@"The `length` parameter cannot be negative.", @"The parameter cannot be negative.")]
+ [InlineData(@"See `System.ComponentModel.DataAnnotations.Validator`.", @"See .")]
+ public void ReplacesMarkdownBacktickReferencesWithTags(string markdown, string expected)
+ {
+ var xml = $@"";
+
+ var testDoc = new TestDocsApi();
+ var typeParamT = new DocsTypeParam(testDoc, XElement.Parse(@"The struct."));
+ var paramLength = new DocsParam(testDoc, XElement.Parse(@"The length."));
+
+ var remarks = new DocsRemarks(XElement.Parse(xml))
+ {
+ TypeParams = new[] { typeParamT },
+ Params = new[] { paramLength }
+ };
+
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+ ## EXAMPLE
+ EXAMPLE CONTENT
+ ")]
+ [InlineData(@"
+
+ ##EXAMPLES
+ EXAMPLE CONTENT
+ ")]
+ public void RemovesExamplesFromRemarks(string xml)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.DoesNotContain("EXAMPLE CONTENT", remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+ ## EXAMPLE
+ EXAMPLE CONTENT
+ ",
+ @"")]
+ [InlineData(@"
+
+ ##EXAMPLES
+ EXAMPLE CONTENT
+ ",
+ @"")]
+ [InlineData(@"
+
+ ## REMARKS
+
+ REMARK CONTENT
+
+ ##EXAMPLES
+
+ EXAMPLE CONTENT
+ ",
+ @"REMARK CONTENT")]
+ public void TrimsRemarksAfterRemovingExamples(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ParsedText);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+ ## EXAMPLE
+ EXAMPLE CONTENT
+ ",
+ @"EXAMPLE CONTENT")]
+ [InlineData(@"
+
+ ##EXAMPLES
+ EXAMPLE CONTENT
+ ",
+ @"EXAMPLE CONTENT")]
+ [InlineData(@"
+
+ ## REMARKS
+
+ REMARK CONTENT
+
+ ##EXAMPLES
+
+ EXAMPLE CONTENT
+ ",
+ @"EXAMPLE CONTENT")]
+ public void GetsExampleContent(string xml, string expected)
+ {
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ Assert.Equal(expected, remarks.ExampleContent?.ParsedText);
+ }
+
+ [Fact]
+ public void GetsNodes()
+ {
+ var xml = @"Remarks referencing a .";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Remarks referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), remarks.RawNodes.ToArray().Select(x => x.ToString()));
+ }
+
+ [Fact]
+ public void CanIncludeSeeElements()
+ {
+ var xml = @"";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ var see = remarks.RawNodes.Single();
+
+ Assert.Equal(XmlNodeType.Element, see.NodeType);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeElements()
+ {
+ var xml = @"";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ var see = remarks.RawNodes.Single();
+
+ Assert.Equal("see", ((XElement)see).Name);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeCrefValues()
+ {
+ var xml = @"";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+ var see = remarks.RawNodes.Single();
+
+ Assert.Equal("T:System.Type", ((XElement)see).Attribute("cref").Value);
+ }
+
+ [Fact]
+ public void ParsesNodes()
+ {
+ var xml = @"Remarks referencing a .";
+ var remarks = new DocsRemarks(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Remarks referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), remarks.ParsedNodes.ToArray());
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsSummaryTests.cs b/Tests/DocsApi/DocsSummaryTests.cs
new file mode 100644
index 0000000..0d76a14
--- /dev/null
+++ b/Tests/DocsApi/DocsSummaryTests.cs
@@ -0,0 +1,139 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsSummaryTests
+ {
+ [Theory]
+ [InlineData(
+ @"Summary.",
+ @"Summary.")]
+ [InlineData(
+ @"Summary referencing .",
+ @"Summary referencing .")]
+ [InlineData(
+ @"
+ Multiline
+ Summary
+ Referencing
+ .
+ ",
+ @"
+ Multiline
+ Summary
+ Referencing
+ .
+ ")]
+ public void GetsRawText(string xml, string expected)
+ {
+ var summary = new DocsSummary(XElement.Parse(xml));
+ Assert.Equal(expected, summary.RawText);
+ }
+
+ [Theory]
+ [InlineData(
+ @"Summary.",
+ @"Summary.")]
+ [InlineData(
+ @"Summary referencing .",
+ @"Summary referencing .")]
+ [InlineData(
+ @"
+ Multiline
+
+ Summary
+
+ Referencing
+
+
+
+ With Blank Lines.
+ ",
+ @"Multiline
+Summary
+Referencing
+
+With Blank Lines.")]
+ public void GetsParsedText(string xml, string expected)
+ {
+ var summary = new DocsSummary(XElement.Parse(xml));
+ Assert.Equal(expected, summary.ParsedText);
+ }
+
+ [Fact]
+ public void AllowsInlineIncludesInParsedText()
+ {
+ var xml = @"Converts narrow (single-byte) characters in the string to wide (double-byte) characters. Applies to Asian locales. This member is equivalent to the Visual Basic constant . [!INCLUDE[vbstrconv-wide](~/includes/vbstrconv-wide-md.md)]";
+ var expected = @"Converts narrow (single-byte) characters in the string to wide (double-byte) characters. Applies to Asian locales. This member is equivalent to the Visual Basic constant . [!INCLUDE[vbstrconv-wide](~/includes/vbstrconv-wide-md.md)]";
+
+ var summary = new DocsSummary(XElement.Parse(xml));
+ Assert.Equal(expected, summary.ParsedText);
+ }
+
+ [Fact]
+ public void GetsNodes()
+ {
+ var xml = @"Summary referencing a .";
+ var summary = new DocsSummary(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Summary referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), summary.RawNodes.ToArray().Select(x => x.ToString()));
+ }
+
+ [Fact]
+ public void CanIncludeSeeElements()
+ {
+ var xml = @"";
+ var summary = new DocsSummary(XElement.Parse(xml));
+ var see = summary.RawNodes.Single();
+
+ Assert.Equal(XmlNodeType.Element, see.NodeType);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeElements()
+ {
+ var xml = @"";
+ var summary = new DocsSummary(XElement.Parse(xml));
+ var see = summary.RawNodes.Single();
+
+ Assert.Equal("see", ((XElement)see).Name);
+ }
+
+ [Fact]
+ public void CanExposeRawSeeCrefValues()
+ {
+ var xml = @"";
+ var summary = new DocsSummary(XElement.Parse(xml));
+ var see = summary.RawNodes.Single();
+
+ Assert.Equal("T:System.Type", ((XElement)see).Attribute("cref").Value);
+ }
+
+ [Fact]
+ public void ParsesNodes()
+ {
+ var xml = @"Summary referencing a .";
+ var summary = new DocsSummary(XElement.Parse(xml));
+
+ var expected = new XNode[]
+ {
+ new XText("Summary referencing a "),
+ XElement.Parse(@""),
+ new XText(".")
+ };
+
+ Assert.Equal(expected.Select(x => x.ToString()), summary.ParsedNodes.ToArray().Select(x => x.ToString()));
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsTypeParamTests.cs b/Tests/DocsApi/DocsTypeParamTests.cs
new file mode 100644
index 0000000..b5401cf
--- /dev/null
+++ b/Tests/DocsApi/DocsTypeParamTests.cs
@@ -0,0 +1,35 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsTypeParamTests
+ {
+ [Theory]
+ [InlineData(
+ @"The type of the keys in the dictionary.",
+ @"TKey")]
+ public void ExtractsName(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var typeParam = new DocsTypeParam(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, typeParam.Name);
+ }
+
+ [Theory]
+ [InlineData(
+ @"The type of the keys in the dictionary.",
+ @"The type of the keys in the dictionary.")]
+ [InlineData(
+ @"The type of items in the .",
+ @"The type of items in the .")]
+ public void ExtractsValueAsPlainText(string xml, string expected)
+ {
+ var parent = new TestDocsApi();
+ var typeParam = new DocsTypeParam(parent, XElement.Parse(xml));
+
+ Assert.Equal(expected, typeParam.Value);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsTypeParameterTests.cs b/Tests/DocsApi/DocsTypeParameterTests.cs
new file mode 100644
index 0000000..6b39b44
--- /dev/null
+++ b/Tests/DocsApi/DocsTypeParameterTests.cs
@@ -0,0 +1,80 @@
+using System.Collections.Generic;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsTypeParameterTests
+ {
+ [Theory]
+ [InlineData(
+ @"",
+ @"T")]
+ [InlineData(@"
+
+
+ Covariant
+
+ ",
+ @"T")]
+ public void ExtractsName(string xml, string expected)
+ {
+ var typeParameter = new DocsTypeParameter(XElement.Parse(xml));
+ Assert.Equal(expected, typeParameter.Name);
+ }
+
+ [Theory]
+ [InlineData(
+ @"",
+ new string[] { })]
+ [InlineData(@"
+
+
+ Covariant
+
+ ",
+ new string[] { "Covariant" })]
+ [InlineData(@"
+
+
+ DefaultConstructorConstraint
+ NotNullableValueTypeConstraint
+ System.ValueType
+
+ ",
+ new string[] { "DefaultConstructorConstraint", "NotNullableValueTypeConstraint" })]
+ public void ExtractsConstraintsParameterAttributesAsPlainText(string xml, IEnumerable expected)
+ {
+ var typeParameter = new DocsTypeParameter(XElement.Parse(xml));
+ Assert.Equal(expected, typeParameter.ConstraintsParameterAttributes);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+
+ System.Data.DataRow
+
+ ",
+ @"System.Data.DataRow")]
+ public void ExtractsConstraintsBaseTypeName(string xml, string expected)
+ {
+ var typeParameter = new DocsTypeParameter(XElement.Parse(xml));
+ Assert.Equal(expected, typeParameter.ConstraintsBaseTypeName);
+ }
+
+ [Theory]
+ [InlineData(@"
+
+
+ System.IComparable<T>
+
+ ",
+ @"System.IComparable<T>")]
+ public void ExtractsConstraintsInterfaceName(string xml, string expected)
+ {
+ var typeParameter = new DocsTypeParameter(XElement.Parse(xml));
+ Assert.Equal(expected, typeParameter.ConstraintsInterfaceName);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsTypeSignatureTests.cs b/Tests/DocsApi/DocsTypeSignatureTests.cs
new file mode 100644
index 0000000..265e7f3
--- /dev/null
+++ b/Tests/DocsApi/DocsTypeSignatureTests.cs
@@ -0,0 +1,34 @@
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsTypeSignatureTests
+ {
+ [Theory]
+ [InlineData(
+ @"",
+ @"C#")]
+ [InlineData(
+ @"",
+ @"VB.NET")]
+ public void ExtractsLanguage(string xml, string expected)
+ {
+ var typeSignature = new DocsTypeSignature(XElement.Parse(xml));
+ Assert.Equal(expected, typeSignature.Language);
+ }
+
+ [Theory]
+ [InlineData(
+ @"",
+ @"public sealed class RequiredAttribute : Attribute")]
+ [InlineData(
+ @"",
+ "Public NotInheritable Class RequiredAttribute \nInherits Attribute")]
+ public void ExtractsValue(string xml, string expected)
+ {
+ var typeSignature = new DocsTypeSignature(XElement.Parse(xml));
+ Assert.Equal(expected, typeSignature.Value);
+ }
+ }
+}
diff --git a/Tests/DocsApi/DocsTypeTests.cs b/Tests/DocsApi/DocsTypeTests.cs
new file mode 100644
index 0000000..387f049
--- /dev/null
+++ b/Tests/DocsApi/DocsTypeTests.cs
@@ -0,0 +1,45 @@
+using System.Text;
+using System.Xml.Linq;
+using Xunit;
+
+namespace Libraries.Docs.Tests
+{
+ public class DocsTypeTests
+ {
+ [Theory]
+ [InlineData( // No remarks
+ @"",
+ @"")]
+ [InlineData( // Plain text
+ @"These are remarks",
+ @"These are remarks")]
+ [InlineData( // With < and > sequences
+ @"These are remarks with <xml> like content",
+ @"These are remarks with <xml> like content")]
+ [InlineData( // With markdown content
+ @"
+
+ HTML embedded.
+
+ ]]>
+
+ ",
+ @"HTML embedded.
+
+ ]]>"
+ )]
+ public void ExtractsRemarksAsPlainText(string xml, string expected)
+ {
+ var doc = XDocument.Parse(@$"{xml}");
+ var type = new DocsType("MyType.xml", doc, doc.Root, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
+
+ Assert.Equal(expected, type.Remarks);
+ }
+ }
+}
diff --git a/Tests/DocsApi/TestDocsApi.cs b/Tests/DocsApi/TestDocsApi.cs
new file mode 100644
index 0000000..01d0ebe
--- /dev/null
+++ b/Tests/DocsApi/TestDocsApi.cs
@@ -0,0 +1,18 @@
+using System.Xml.Linq;
+
+namespace Libraries.Docs.Tests
+{
+ class TestDocsApi : DocsAPI
+ {
+ public TestDocsApi() : base(XElement.Parse("")) { }
+
+ public override bool Changed { get; set; }
+
+ public override string DocId => "--DocId--";
+
+ public override string Summary { get; set; }
+ public override string Remarks { get; set; }
+ public override string ReturnType { get; }
+ public override string Returns { get; set; }
+ }
+}
diff --git a/Tests/PortToDocs/PortToDocsTests.cs b/Tests/PortToDocs/PortToDocsTests.cs
index a780839..786a724 100644
--- a/Tests/PortToDocs/PortToDocsTests.cs
+++ b/Tests/PortToDocs/PortToDocsTests.cs
@@ -35,7 +35,7 @@ public void Port_AssemblyAndNamespaceDifferent()
{
PortToDocs("AssemblyAndNamespaceDifferent",
GetConfiguration(),
- namespaceNames: new[] { TestData.TestNamespace});
+ namespaceNames: new[] { TestData.TestNamespace });
}
[Fact]
@@ -153,7 +153,7 @@ private static Configuration GetConfiguration(
SkipInterfaceRemarks = skipInterfaceRemarks
};
- private static void PortToDocs(
+ private static void PortToDocs(
string testName,
Configuration c,
string[] assemblyNames = null,
diff --git a/Tests/PortToTripleSlash/LeadingTriviaRewriterTests.cs b/Tests/PortToTripleSlash/LeadingTriviaRewriterTests.cs
new file mode 100644
index 0000000..50fed69
--- /dev/null
+++ b/Tests/PortToTripleSlash/LeadingTriviaRewriterTests.cs
@@ -0,0 +1,195 @@
+#nullable enable
+using Libraries.RoslynTripleSlash;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Xunit;
+
+namespace Libraries.Tests.PortToTripleSlash
+{
+ public class LeadingTriviaRewriterTests
+ {
+ public struct LeadingTriviaTestFile
+ {
+ public SyntaxNode MyType;
+ public SyntaxNode MyEnum;
+ public SyntaxNode MyField;
+ public SyntaxNode MyProperty;
+ public SyntaxNode MyMethod;
+ public SyntaxNode MyInterface;
+ }
+
+ private static LeadingTriviaTestFile LoadTestFile(string fileName)
+ {
+ // We rely on the test data files being marked as Content
+ // and being copied to the output directory
+ string testFolder = "./PortToTripleSlash/TestData/LeadingTrivia";
+ string testContent = File.ReadAllText(Path.Combine(testFolder, fileName));
+
+ IEnumerable nodes = SyntaxFactory.ParseSyntaxTree(testContent).GetRoot().DescendantNodes();
+
+ return new LeadingTriviaTestFile
+ {
+ MyType = nodes.First(n => n.IsKind(SyntaxKind.ClassDeclaration)),
+ MyEnum = nodes.First(n => n.IsKind(SyntaxKind.EnumDeclaration)),
+ MyField = nodes.First(n => n.IsKind(SyntaxKind.FieldDeclaration)),
+ MyProperty = nodes.First(n => n.IsKind(SyntaxKind.PropertyDeclaration)),
+ MyMethod = nodes.First(n => n.IsKind(SyntaxKind.MethodDeclaration)),
+ MyInterface = nodes.First(n => n.IsKind(SyntaxKind.InterfaceDeclaration))
+ };
+ }
+
+ private static (LeadingTriviaTestFile Original, LeadingTriviaTestFile Expected) LoadTestFiles(string test)
+ {
+ LeadingTriviaTestFile original = LoadTestFile($"{test}.Original.cs");
+ LeadingTriviaTestFile expected = LoadTestFile($"{test}.Expected.cs");
+
+ return (original, expected);
+ }
+
+ public static IEnumerable
diff --git a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml
index 475a62a..da746c2 100644
--- a/Tests/PortToTripleSlash/TestData/Basic/MyType.xml
+++ b/Tests/PortToTripleSlash/TestData/Basic/MyType.xml
@@ -20,9 +20,9 @@ Multiple lines.
> [!NOTE]
> This note should prevent converting markdown to xml. It has a .
-This text is not a note. It has a that should be xml and outside **the cdata**.
+This text is not a note. It has a that should remain and still be **inside the cdata**.
-Long xrefs one after the other: or should both be converted to crefs.
+Long xrefs one after the other: or should also remain.
]]>
@@ -122,7 +122,7 @@ Here is a random snippet, NOT preceded by the examples header.
[!code-cpp[MyExample](~/samples/snippets/example.cpp)]
-There is a hyperlink, which should still allow conversion from markdown to xml: [MyHyperlink](http://github.com/dotnet/runtime).
+There is a hyperlink, which should remain as markdown: [MyHyperlink](http://github.com/dotnet/runtime).
]]>
diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.verified.cs
similarity index 85%
rename from Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs
rename to Tests/PortToTripleSlash/TestData/Basic/SourceExpected.verified.cs
index a93b747..a2d6bea 100644
--- a/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.cs
+++ b/Tests/PortToTripleSlash/TestData/Basic/SourceExpected.verified.cs
@@ -18,20 +18,20 @@ public enum MyEnum
}
/// This is the MyType class summary.
- /// These are the class remarks.
- /// URL entities: #(),.
+ /// class remarks.
+ /// URL entities: %23%28%29%2C.
/// Multiple lines.
- /// [!NOTE]
/// > This note should prevent converting markdown to xml. It has a .
- /// ]]>
- /// This text is not a note. It has a that should be xml and outside the cdata.
- /// Long xrefs one after the other: or should both be converted to crefs.
+ /// This text is not a note. It has a that should remain and still be **inside the cdata**.
+ /// Long xrefs one after the other: or should also remain.
+ /// ]]>
// Original MyType class comments with information for maintainers, must stay.
public class MyType
{
+ // Original MyType constructor double slash comments on top of triple slash, with information for maintainers, must stay.
/// This is the MyType constructor summary.
- // Original MyType constructor double slash comments on top of triple slash, with information for maintainers, must stay but after triple slash.
// Original MyType constructor double slash comments on bottom of triple slash, with information for maintainers, must stay.
public MyType()
{
@@ -86,12 +86,12 @@ public int MyProperty
/// This is the MyIntMethod return value. It mentions the .
/// This is the ArgumentNullException thrown by MyIntMethod. It mentions the .
/// This is the IndexOutOfRangeException thrown by MyIntMethod.
- /// These are the MyIntMethod remarks.
+ ///
- /// There is a hyperlink, which should still allow conversion from markdown to xml: MyHyperlink.
+ /// There is a hyperlink, which should remain as markdown: [MyHyperlink](http://github.com/dotnet/runtime).
+ /// ]]>
public int MyIntMethod(int param1, int param2)
{
// Internal comments should remain untouched.
@@ -101,14 +101,17 @@ public int MyIntMethod(int param1, int param2)
/// This is the MyVoidMethod summary.
/// This is the ArgumentNullException thrown by MyVoidMethod. It mentions the .
/// This is the IndexOutOfRangeException thrown by MyVoidMethod.
+ ///
/// -or-
+ ///
/// This is the second case.
+ ///
/// Empty newlines should be respected.
/// These are the MyVoidMethod remarks.
/// Multiple lines.
/// Mentions the .
- /// Also mentions an overloaded method DocID: .
- /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: .
+ /// Also mentions an overloaded method DocID: .
+ /// And also mentions an overloaded method DocID with displayProperty which should be ignored when porting: .
public void MyVoidMethod()
{
}
@@ -141,14 +144,14 @@ public void MyTypeParamMethod(int param1)
/// This is the sender parameter.
/// This is the e parameter.
/// These are the remarks. There is a code example, which should be moved to its own examples section:
- /// Here is some text in the examples section. There is an that should be converted to xml.
+ /// that should remain an xref element.
/// The snippet links below should be inserted in markdown.
- ///
- /// This text should be outside the cdata in xml: .
+ /// This text remain inside the cdata: .
+ /// ]]>
///
///
/// The .NET Runtime repo.
diff --git a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs
index 449065a..4fc77d8 100644
--- a/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs
+++ b/Tests/PortToTripleSlash/TestData/Basic/SourceOriginal.cs
@@ -13,7 +13,7 @@ public enum MyEnum
// Original MyType class comments with information for maintainers, must stay.
public class MyType
{
- // Original MyType constructor double slash comments on top of triple slash, with information for maintainers, must stay but after triple slash.
+ // Original MyType constructor double slash comments on top of triple slash, with information for maintainers, must stay.
///
/// Original triple slash comments. They should be replaced.
///
diff --git a/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml
new file mode 100644
index 0000000..b354acf
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType.xml
@@ -0,0 +1,30 @@
+
+
+
+ MyAssembly
+
+
+ This is the MyGenericType static class summary.
+
+
+
+
+
+ Projects each element into a new form.
+ The type of the elements of .
+ The type of the value returned by .
+ A sequence of values to invoke a transform function on.
+ A transform function to apply to each element.
+
+
+
+ .
+ ]]>
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml
index a858375..d2e23a2 100644
--- a/Tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml
+++ b/Tests/PortToTripleSlash/TestData/Generics/MyGenericType`1.xml
@@ -9,7 +9,7 @@
.
+The `MyGenericType` type contains the nested class .
]]>
diff --git a/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs b/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs
deleted file mode 100644
index ae85c0f..0000000
--- a/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-
-namespace MyNamespace
-{
- /// This is the MyGenericType{T} class summary.
- /// Contains the nested class .
- // Original MyGenericType class comments with information for maintainers, must stay.
- public class MyGenericType
- {
- /// This is the MyGenericType{T}.Enumerator class summary.
- // Original MyGenericType.Enumerator class comments with information for maintainers, must stay.
- public class Enumerator { }
- }
-}
diff --git a/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.verified.cs b/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.verified.cs
new file mode 100644
index 0000000..1c27771
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/Generics/SourceExpected.verified.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace MyNamespace
+{
+ /// This is the MyGenericType{T} class summary.
+ /// The type contains the nested class .
+ // Original MyGenericType class comments with information for maintainers, must stay.
+ public class MyGenericType
+ {
+ /// This is the MyGenericType{T}.Enumerator class summary.
+ // Original MyGenericType.Enumerator class comments with information for maintainers, must stay.
+ public class Enumerator { }
+ }
+
+ /// This is the MyGenericType static class summary.
+ public static class MyGenericType
+ {
+ /// Projects each element into a new form.
+ /// The type of the elements of .
+ /// The type of the value returned by .
+ /// A sequence of values to invoke a transform function on.
+ /// A transform function to apply to each element.
+ /// Here's a reference to .
+ ///
+ public static MyGenericType Select(this MyGenericType source, Func selector) => null;
+ }
+}
diff --git a/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs b/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs
index 3d91be3..6e8d502 100644
--- a/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs
+++ b/Tests/PortToTripleSlash/TestData/Generics/SourceOriginal.cs
@@ -8,4 +8,9 @@ public class MyGenericType
// Original MyGenericType.Enumerator class comments with information for maintainers, must stay.
public class Enumerator { }
}
+
+ public static class MyGenericType
+ {
+ public static MyGenericType Select(this MyGenericType source, Func selector) => null;
+ }
}
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Expected.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Expected.cs
new file mode 100644
index 0000000..d15dd61
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Expected.cs
@@ -0,0 +1,69 @@
+namespace LeadingTriviaTestData.Directives.Expected
+{
+#if false
+ internal
+#else
+ /// Directives
+ /// Directives
+ public
+#endif
+ class MyType
+ {
+ #region MyEnum
+
+#if true
+ /// Directives
+ /// Directives
+ public
+#else
+ internal
+#endif
+ enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ #endregion
+
+#pragma warning disable
+ /// Directives
+ /// Directives
+ // This comment should remain below the XML comments
+ public int MyField;
+#pragma warning restore
+
+#nullable enable
+ /// Directives
+ /// Directives
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+#nullable restore
+
+#if true
+ /// Directives
+ /// Directives
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ /// Directives
+ /// Directives
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Original.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Original.cs
new file mode 100644
index 0000000..43585d3
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/Directives.Original.cs
@@ -0,0 +1,57 @@
+namespace LeadingTriviaTestData.Directives.Original
+{
+#if false
+ internal
+#else
+ public
+#endif
+ class MyType
+ {
+ #region MyEnum
+
+#if true
+ public
+#else
+ internal
+#endif
+ enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ #endregion
+
+#pragma warning disable
+ // This comment should remain below the XML comments
+ public int MyField;
+#pragma warning restore
+
+#nullable enable
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+#nullable restore
+
+#if true
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Expected.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Expected.cs
new file mode 100644
index 0000000..e85d11d
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Expected.cs
@@ -0,0 +1,69 @@
+namespace LeadingTriviaTestData.DirectivesExistingXml.Expected
+{
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+#if false
+ internal
+#else
+ public
+#endif
+ class MyType
+ {
+ #region MyEnum
+
+#if true
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+ public
+#else
+ internal
+#endif
+ enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ #endregion
+
+#pragma warning disable
+ // This comment should remain above the XML comments
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+ public int MyField;
+#pragma warning restore
+
+#nullable enable
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+#nullable restore
+
+#if true
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ /// DirectivesExistingXml
+ /// DirectivesExistingXml
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Original.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Original.cs
new file mode 100644
index 0000000..4da7076
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/DirectivesExistingXml.Original.cs
@@ -0,0 +1,93 @@
+namespace LeadingTriviaTestData.DirectivesExistingXml.Original
+{
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+#if false
+ internal
+#else
+ public
+#endif
+ class MyType
+ {
+ #region MyEnum
+
+#if true
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+ public
+#else
+ internal
+#endif
+ enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ #endregion
+
+#pragma warning disable
+ // This comment should remain above the XML comments
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+ public int MyField;
+#pragma warning restore
+
+#nullable enable
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+#nullable restore
+
+#if true
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ ///
+ /// This is the original summary
+ ///
+ ///
+ /// These are the existing remarks
+ ///
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Expected.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Expected.cs
new file mode 100644
index 0000000..0750260
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Expected.cs
@@ -0,0 +1,66 @@
+namespace LeadingTriviaTestData.ExistingXml.Expected
+{
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public class MyType
+ {
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public int MyField;
+
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ // Single line comment to keep above the XML comments
+ /// ExistingXml
+ /// ExistingXml
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Original.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Original.cs
new file mode 100644
index 0000000..1f4529c
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/ExistingXml.Original.cs
@@ -0,0 +1,90 @@
+namespace LeadingTriviaTestData.ExistingXml.Original
+{
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public class MyType
+ {
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public int MyField;
+
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ // Single line comment to keep above the XML comments
+ ///
+ /// This was the original summary
+ ///
+ ///
+ /// These were the original remarks
+ ///
+ /* Multi-line comment to keep
+ * below the XML comments */
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Expected.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Expected.cs
new file mode 100644
index 0000000..6b8cc45
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Expected.cs
@@ -0,0 +1,48 @@
+namespace LeadingTriviaTestData.WhitespaceOnly.Expected
+{
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public class MyType
+ {
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public int MyField;
+
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ /// WhitespaceOnly
+ /// WhitespaceOnly
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Original.cs b/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Original.cs
new file mode 100644
index 0000000..34610f4
--- /dev/null
+++ b/Tests/PortToTripleSlash/TestData/LeadingTrivia/WhitespaceOnly.Original.cs
@@ -0,0 +1,36 @@
+namespace LeadingTriviaTestData.WhitespaceOnly.Original
+{
+ public class MyType
+ {
+ public enum MyEnum
+ {
+ FirstValue = 1,
+ SecondValue,
+ ThirdValue,
+ }
+
+ public int MyField;
+
+ public string MyProperty
+ {
+ get
+ {
+ return "";
+ }
+ set
+ {
+
+ }
+ }
+
+ public bool MyMethod()
+ {
+ return true;
+ }
+
+ public interface MyInterface
+ {
+ bool IsInterface { get; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tests/TestData.cs b/Tests/TestData.cs
index ca56dd5..f579c85 100644
--- a/Tests/TestData.cs
+++ b/Tests/TestData.cs
@@ -9,7 +9,6 @@ internal class TestData
internal const string TestType = "MyType";
internal const string DocsDirName = "Docs";
- internal string ExpectedFilePath { get; set; }
internal string ActualFilePath { get; set; }
internal DirectoryInfo DocsDir { get; set; }
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index ed8c457..ae11375 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -5,41 +5,40 @@
Microsoft
carlossanlop
false
+ enable
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+ PreserveNewest
+
+
+ PreserveNewest
+
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+