forked from domaindrivendev/Swashbuckle.AspNetCore
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathXmlCommentsTextHelper.cs
More file actions
244 lines (198 loc) · 8.89 KB
/
XmlCommentsTextHelper.cs
File metadata and controls
244 lines (198 loc) · 8.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace Swashbuckle.AspNetCore.SwaggerGen
{
public static partial class XmlCommentsTextHelper
{
public static string Humanize(string text)
{
return Humanize(text, null);
}
public static string Humanize(string text, string xmlCommentEndOfLine)
{
if (text == null)
throw new ArgumentNullException(nameof(text));
//Call DecodeXml at last to avoid entities like < and > to break valid xml
return text
.NormalizeIndentation(xmlCommentEndOfLine)
.HumanizeRefTags()
.HumanizeHrefTags()
.HumanizeCodeTags()
.HumanizeMultilineCodeTags(xmlCommentEndOfLine)
.HumanizeParaTags()
.HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional <br> tags
.DecodeXml();
}
private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine)
{
string[] lines = text.Split('\n');
string padding = GetCommonLeadingWhitespace(lines);
int padLen = padding == null ? 0 : padding.Length;
// remove leading padding from each line
for (int i = 0, l = lines.Length; i < l; ++i)
{
string line = lines[i].TrimEnd('\r'); // remove trailing '\r'
if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding)
line = line.Substring(padLen);
lines[i] = line;
}
// remove leading empty lines, but not all leading padding
// remove all trailing whitespace, regardless
return string.Join(xmlCommentEndOfLine ?? "\r\n", lines.SkipWhile(x => string.IsNullOrWhiteSpace(x))).TrimEnd();
}
private static string GetCommonLeadingWhitespace(string[] lines)
{
if (null == lines)
throw new ArgumentException("lines");
if (lines.Length == 0)
return null;
string[] nonEmptyLines = lines
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToArray();
if (nonEmptyLines.Length < 1)
return null;
int padLen = 0;
// use the first line as a seed, and see what is shared over all nonEmptyLines
string seed = nonEmptyLines[0];
for (int i = 0, l = seed.Length; i < l; ++i)
{
if (!char.IsWhiteSpace(seed, i))
break;
if (nonEmptyLines.Any(line => line[i] != seed[i]))
break;
++padLen;
}
if (padLen > 0)
return seed.Substring(0, padLen);
return null;
}
private static string HumanizeRefTags(this string text)
{
return RefTag().Replace(text, (match) => match.Groups["display"].Value);
}
private static string HumanizeHrefTags(this string text)
{
return HrefTag().Replace(text, m => $"[{m.Groups[2].Value}]({m.Groups[1].Value})");
}
private static string HumanizeCodeTags(this string text)
{
return CodeTag().Replace(text, (match) => "`" + match.Groups["display"].Value + "`");
}
private static string HumanizeMultilineCodeTags(this string text, string xmlCommentEndOfLine)
{
return MultilineCodeTag().Replace(text, match =>
{
var codeText = match.Groups["display"].Value;
if (LineBreaks().IsMatch(codeText))
{
var builder = new StringBuilder().Append("```");
if (!codeText.StartsWith("\r") && !codeText.StartsWith("\n"))
{
builder.Append(EndOfLine(xmlCommentEndOfLine));
}
builder.Append(RemoveCommonLeadingWhitespace(codeText, xmlCommentEndOfLine));
if (!codeText.EndsWith("\n"))
{
builder.Append(EndOfLine(xmlCommentEndOfLine));
}
builder.Append("```");
return DoubleUpLineBreaks().Replace(builder.ToString(), EndOfLine(xmlCommentEndOfLine));
}
return $"```{codeText}```";
});
}
private static string HumanizeParaTags(this string text)
{
return ParaTag().Replace(text, match => "<br>" + match.Groups["display"].Value.Trim());
}
private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine)
{
return BrTag().Replace(text, _ => EndOfLine(xmlCommentEndOfLine));
}
private static string DecodeXml(this string text)
{
return WebUtility.HtmlDecode(text);
}
private static string RemoveCommonLeadingWhitespace(string input, string xmlCommentEndOfLine)
{
var lines = input.Split(["\r\n", "\n"], StringSplitOptions.None);
var minLeadingSpaces = int.MaxValue;
foreach (var line in lines)
{
if (string.IsNullOrEmpty(line))
{
continue;
}
var leadingSpaces = line.Length - line.TrimStart(' ').Length;
minLeadingSpaces = Math.Min(minLeadingSpaces, leadingSpaces);
if (minLeadingSpaces == 0)
{
return input;
}
}
if (minLeadingSpaces is int.MaxValue)
{
return input;
}
var builder = new StringBuilder();
foreach (var line in lines)
{
builder.Append(string.IsNullOrWhiteSpace(line)
? line
: line.Substring(minLeadingSpaces));
builder.Append(EndOfLine(xmlCommentEndOfLine));
}
return builder.ToString();
}
private static string EndOfLine(string xmlCommentEndOfLine)
{
return xmlCommentEndOfLine ?? Environment.NewLine;
}
private const string RefTagPattern = @"<(see|paramref) (name|cref|langword)=""([TPF]{1}:)?(?<display>.+?)"" ?/>";
private const string CodeTagPattern = @"<c>(?<display>.+?)</c>";
private const string MultilineCodeTagPattern = @"<code>(?<display>.+?)</code>";
private const string ParaTagPattern = @"<para>(?<display>.+?)</para>";
private const string HrefPattern = @"<see href=\""(.*)\"">(.*)<\/see>";
private const string BrPattern = @"(<br ?\/?>)"; // handles <br>, <br/>, <br />
private const string LineBreaksPattern = @"\r?\n";
private const string DoubleUpLineBreaksPattern = @"(\r?\n){2,}";
#if NET7_0_OR_GREATER
[GeneratedRegex(RefTagPattern)]
private static partial Regex RefTag();
[GeneratedRegex(CodeTagPattern)]
private static partial Regex CodeTag();
[GeneratedRegex(MultilineCodeTagPattern, RegexOptions.Singleline)]
private static partial Regex MultilineCodeTag();
[GeneratedRegex(ParaTagPattern, RegexOptions.Singleline)]
private static partial Regex ParaTag();
[GeneratedRegex(HrefPattern)]
private static partial Regex HrefTag();
[GeneratedRegex(BrPattern)]
private static partial Regex BrTag();
[GeneratedRegex(LineBreaksPattern)]
private static partial Regex LineBreaks();
[GeneratedRegex(DoubleUpLineBreaksPattern)]
private static partial Regex DoubleUpLineBreaks();
#else
private static readonly Regex _refTag = new(RefTagPattern);
private static readonly Regex _codeTag = new(CodeTagPattern);
private static readonly Regex _multilineCodeTag = new(MultilineCodeTagPattern, RegexOptions.Singleline);
private static readonly Regex _paraTag = new(ParaTagPattern, RegexOptions.Singleline);
private static readonly Regex _hrefTag = new(HrefPattern);
private static readonly Regex _brTag = new(BrPattern);
private static readonly Regex _lineBreaks = new(LineBreaksPattern);
private static readonly Regex _doubleUpLineBreaks = new(DoubleUpLineBreaksPattern);
private static Regex RefTag() => _refTag;
private static Regex CodeTag() => _codeTag;
private static Regex MultilineCodeTag() => _multilineCodeTag;
private static Regex ParaTag() => _paraTag;
private static Regex HrefTag() => _hrefTag;
private static Regex BrTag() => _brTag;
private static Regex LineBreaks() => _lineBreaks;
private static Regex DoubleUpLineBreaks() => _doubleUpLineBreaks;
#endif
}
}