Skip to content

Commit 76f8bc6

Browse files
authored
Fix humanize for multiline code and para tags (#3295)
Fix humanize for multiline `<code>` and `<para>` tags Fixes #3282.
1 parent 2efd5e9 commit 76f8bc6

File tree

4 files changed

+110
-51
lines changed

4 files changed

+110
-51
lines changed

src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ public static string Humanize(string text, string xmlCommentEndOfLine)
2525
.HumanizeRefTags()
2626
.HumanizeHrefTags()
2727
.HumanizeCodeTags()
28-
.HumanizeMultilineCodeTags()
28+
.HumanizeMultilineCodeTags(xmlCommentEndOfLine)
2929
.HumanizeParaTags()
3030
.HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional <br> tags
3131
.DecodeXml();
3232
}
3333

3434
private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine)
3535
{
36-
string[] lines = text.Split('\n');
36+
var lines = text.Split(["\r\n", "\n"], StringSplitOptions.None);
3737
string padding = GetCommonLeadingWhitespace(lines);
3838

39-
int padLen = padding == null ? 0 : padding.Length;
39+
int padLen = padding?.Length ?? 0;
4040

4141
// remove leading padding from each line
4242
for (int i = 0, l = lines.Length; i < l; ++i)
@@ -51,7 +51,7 @@ private static string NormalizeIndentation(this string text, string xmlCommentEn
5151

5252
// remove leading empty lines, but not all leading padding
5353
// remove all trailing whitespace, regardless
54-
return string.Join(xmlCommentEndOfLine ?? "\r\n", lines.SkipWhile(x => string.IsNullOrWhiteSpace(x))).TrimEnd();
54+
return string.Join(EndOfLine(xmlCommentEndOfLine), lines.SkipWhile(string.IsNullOrWhiteSpace)).TrimEnd();
5555
}
5656

5757
private static string GetCommonLeadingWhitespace(string[] lines)
@@ -105,7 +105,7 @@ private static string HumanizeCodeTags(this string text)
105105
return CodeTag().Replace(text, (match) => "`" + match.Groups["display"].Value + "`");
106106
}
107107

108-
private static string HumanizeMultilineCodeTags(this string text)
108+
private static string HumanizeMultilineCodeTags(this string text, string xmlCommentEndOfLine)
109109
{
110110
return MultilineCodeTag().Replace(text, match =>
111111
{
@@ -115,12 +115,17 @@ private static string HumanizeMultilineCodeTags(this string text)
115115
var builder = new StringBuilder().Append("```");
116116
if (!codeText.StartsWith("\r") && !codeText.StartsWith("\n"))
117117
{
118-
builder.AppendLine();
118+
builder.Append(EndOfLine(xmlCommentEndOfLine));
119119
}
120120

121-
return builder.AppendLine(codeText.TrimEnd())
122-
.Append("```")
123-
.ToString();
121+
builder.Append(RemoveCommonLeadingWhitespace(codeText, xmlCommentEndOfLine));
122+
if (!codeText.EndsWith("\n"))
123+
{
124+
builder.Append(EndOfLine(xmlCommentEndOfLine));
125+
}
126+
127+
builder.Append("```");
128+
return DoubleUpLineBreaks().Replace(builder.ToString(), EndOfLine(xmlCommentEndOfLine));
124129
}
125130

126131
return $"```{codeText}```";
@@ -129,30 +134,54 @@ private static string HumanizeMultilineCodeTags(this string text)
129134

130135
private static string HumanizeParaTags(this string text)
131136
{
132-
return ParaTag().Replace(text, match =>
133-
{
134-
var paraText = "<br>" + match.Groups["display"].Value.Trim();
135-
return LineBreaks().Replace(paraText, _ => string.Empty);
136-
});
137+
return ParaTag().Replace(text, match => "<br>" + match.Groups["display"].Value.Trim());
137138
}
138139

139140
private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine)
140141
{
141-
return BrTag().Replace(text, _ => xmlCommentEndOfLine ?? Environment.NewLine);
142+
return BrTag().Replace(text, _ => EndOfLine(xmlCommentEndOfLine));
142143
}
143144

144145
private static string DecodeXml(this string text)
145146
{
146147
return WebUtility.HtmlDecode(text);
147148
}
148149

150+
private static string RemoveCommonLeadingWhitespace(string input, string xmlCommentEndOfLine)
151+
{
152+
var lines = input.Split(["\r\n", "\n"], StringSplitOptions.None);
153+
var padding = GetCommonLeadingWhitespace(lines);
154+
if (string.IsNullOrEmpty(padding))
155+
{
156+
return input;
157+
}
158+
159+
var minLeadingSpaces = padding.Length;
160+
var builder = new StringBuilder();
161+
foreach (var line in lines)
162+
{
163+
builder.Append(string.IsNullOrWhiteSpace(line)
164+
? line
165+
: line.Substring(minLeadingSpaces));
166+
builder.Append(EndOfLine(xmlCommentEndOfLine));
167+
}
168+
169+
return builder.ToString();
170+
}
171+
172+
internal static string EndOfLine(string xmlCommentEndOfLine)
173+
{
174+
return xmlCommentEndOfLine ?? Environment.NewLine;
175+
}
176+
149177
private const string RefTagPattern = @"<(see|paramref) (name|cref|langword)=""([TPF]{1}:)?(?<display>.+?)"" ?/>";
150178
private const string CodeTagPattern = @"<c>(?<display>.+?)</c>";
151179
private const string MultilineCodeTagPattern = @"<code>(?<display>.+?)</code>";
152180
private const string ParaTagPattern = @"<para>(?<display>.+?)</para>";
153181
private const string HrefPattern = @"<see href=\""(.*)\"">(.*)<\/see>";
154182
private const string BrPattern = @"(<br ?\/?>)"; // handles <br>, <br/>, <br />
155183
private const string LineBreaksPattern = @"\r?\n";
184+
private const string DoubleUpLineBreaksPattern = @"(\r?\n){2,}";
156185

157186
#if NET7_0_OR_GREATER
158187
[GeneratedRegex(RefTagPattern)]
@@ -175,6 +204,9 @@ private static string DecodeXml(this string text)
175204

176205
[GeneratedRegex(LineBreaksPattern)]
177206
private static partial Regex LineBreaks();
207+
208+
[GeneratedRegex(DoubleUpLineBreaksPattern)]
209+
private static partial Regex DoubleUpLineBreaks();
178210
#else
179211
private static readonly Regex _refTag = new(RefTagPattern);
180212
private static readonly Regex _codeTag = new(CodeTagPattern);
@@ -183,6 +215,7 @@ private static string DecodeXml(this string text)
183215
private static readonly Regex _hrefTag = new(HrefPattern);
184216
private static readonly Regex _brTag = new(BrPattern);
185217
private static readonly Regex _lineBreaks = new(LineBreaksPattern);
218+
private static readonly Regex _doubleUpLineBreaks = new(DoubleUpLineBreaksPattern);
186219

187220
private static Regex RefTag() => _refTag;
188221
private static Regex CodeTag() => _codeTag;
@@ -191,6 +224,7 @@ private static string DecodeXml(this string text)
191224
private static Regex HrefTag() => _hrefTag;
192225
private static Regex BrTag() => _brTag;
193226
private static Regex LineBreaks() => _lineBreaks;
227+
private static Regex DoubleUpLineBreaks() => _doubleUpLineBreaks;
194228
#endif
195229
}
196230
}

test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_DotNet6_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"CrudActions"
9494
],
9595
"summary": "Get all products",
96-
"description": "```\r\n {\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n }\r\n```",
96+
"description": "```\r\n{\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n}\r\n \r\n```",
9797
"responses": {
9898
"200": {
9999
"description": "OK",
@@ -199,7 +199,7 @@
199199
"CrudActions"
200200
],
201201
"summary": "Updates some properties of a specific product",
202-
"description": "\r\nOnly provided properties will be updated, other remain unchanged.\r\n\r\nIdentifier must be non-default value\r\n\r\nBody must be specified",
202+
"description": "\r\nOnly provided properties will be updated,\r\n other remain unchanged.\r\n\r\nIdentifier must be non-default value\r\n\r\nBody must be specified",
203203
"operationId": "PatchProduct",
204204
"parameters": [
205205
{

test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
"CrudActions"
9494
],
9595
"summary": "Get all products",
96-
"description": "```\r\n {\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n }\r\n```",
96+
"description": "```\r\n{\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n}\r\n \r\n```",
9797
"responses": {
9898
"200": {
9999
"description": "OK",
@@ -199,7 +199,7 @@
199199
"CrudActions"
200200
],
201201
"summary": "Updates some properties of a specific product",
202-
"description": "\r\nOnly provided properties will be updated, other remain unchanged.\r\n\r\nIdentifier must be non-default value\r\n\r\nBody must be specified",
202+
"description": "\r\nOnly provided properties will be updated,\r\n other remain unchanged.\r\n\r\nIdentifier must be non-default value\r\n\r\nBody must be specified",
203203
"operationId": "PatchProduct",
204204
"parameters": [
205205
{

test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System;
2-
using Xunit;
1+
using Xunit;
32

43
namespace Swashbuckle.AspNetCore.SwaggerGen.Test
54
{
@@ -145,29 +144,10 @@ public void Humanize_HumanizesInlineTags(
145144
Assert.Equal(expectedOutput, output, false, true);
146145
}
147146

148-
[Fact]
149-
public void Humanize_MultilineBrTag_EolNotSpecified()
150-
{
151-
const string input = @"
152-
This is a paragraph.
153-
<br>
154-
A parameter after br tag.";
155-
156-
var output = XmlCommentsTextHelper.Humanize(input);
157-
158-
// Result view for Linux: This is a paragraph.\r\n\n\r\nA parameter after br tag.
159-
var expected = string.Join("\r\n",
160-
[
161-
"This is a paragraph.",
162-
Environment.NewLine,
163-
"A parameter after br tag."
164-
]);
165-
Assert.Equal(expected, output, false, ignoreLineEndingDifferences: false);
166-
}
167-
168147
[Theory]
169148
[InlineData("\r\n")]
170149
[InlineData("\n")]
150+
[InlineData(null)]
171151
public void Humanize_MultilineBrTag_SpecificEol(string xmlCommentEndOfLine)
172152
{
173153
const string input = @"
@@ -177,7 +157,7 @@ This is a paragraph.
177157

178158
var output = XmlCommentsTextHelper.Humanize(input, xmlCommentEndOfLine);
179159

180-
var expected = string.Join(xmlCommentEndOfLine,
160+
var expected = string.Join(XmlCommentsTextHelper.EndOfLine(xmlCommentEndOfLine),
181161
[
182162
"This is a paragraph.",
183163
"",
@@ -199,7 +179,7 @@ This is a paragraph.
199179

200180
var output = XmlCommentsTextHelper.Humanize(input);
201181

202-
Assert.Equal("\r\nThis is a paragraph. MultiLined.\r\n\r\nThis is a paragraph.", output, false, true);
182+
Assert.Equal("\r\nThis is a paragraph.\r\n MultiLined.\r\n\r\nThis is a paragraph.", output, false, true);
203183
}
204184

205185
[Fact]
@@ -215,16 +195,16 @@ public void Humanize_CodeMultiLineTag()
215195

216196
var output = XmlCommentsTextHelper.Humanize(input);
217197

218-
var expected = string.Join("\r\n",
198+
var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null),
219199
[
220200
"```",
221-
" {",
222-
" \"Prop1\":1,",
223-
" \"Prop2\":[]",
224-
" }",
201+
"{",
202+
" \"Prop1\":1,",
203+
" \"Prop2\":[]",
204+
"}",
225205
"```"
226206
]);
227-
Assert.Equal(expected, output, false, true);
207+
Assert.Equal(expected, output);
228208
}
229209

230210
[Fact]
@@ -239,7 +219,7 @@ public void Humanize_CodeMultiLineTag_OnSameLine()
239219

240220
var output = XmlCommentsTextHelper.Humanize(input);
241221

242-
var expected = string.Join("\r\n",
222+
var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null),
243223
[
244224
"```",
245225
"{",
@@ -248,7 +228,52 @@ public void Humanize_CodeMultiLineTag_OnSameLine()
248228
" }",
249229
"```"
250230
]);
251-
Assert.Equal(expected, output, false, true);
231+
Assert.Equal(expected, output);
232+
}
233+
234+
[Fact]
235+
public void Humanize_CodeInsideParaTag()
236+
{
237+
var input = string.Join(XmlCommentsTextHelper.EndOfLine(null),
238+
[
239+
"<para>Creates a new Answer</para>",
240+
"<para><code><![CDATA[",
241+
"POST /api/answers",
242+
"{",
243+
""" "name": "OnlyYes",""",
244+
""" "label": "Yes",""",
245+
""" "answers": [""",
246+
" {",
247+
""" "answer": "yes""",
248+
" }",
249+
" ]",
250+
"}",
251+
"]]></code></para>",
252+
]);
253+
254+
var output = XmlCommentsTextHelper.Humanize(input);
255+
256+
var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null),
257+
[
258+
"",
259+
"Creates a new Answer",
260+
"",
261+
"```",
262+
"<![CDATA[",
263+
"POST /api/answers",
264+
"{",
265+
""" "name": "OnlyYes",""",
266+
""" "label": "Yes",""",
267+
""" "answers": [""",
268+
" {",
269+
""" "answer": "yes""",
270+
" }",
271+
" ]",
272+
"}",
273+
"]]>",
274+
"```"
275+
]);
276+
Assert.Equal(expected, output);
252277
}
253278
}
254279
}

0 commit comments

Comments
 (0)