diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs index 7903a3e2ed..9c19354db6 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs @@ -25,7 +25,7 @@ public static string Humanize(string text, string xmlCommentEndOfLine) .HumanizeRefTags() .HumanizeHrefTags() .HumanizeCodeTags() - .HumanizeMultilineCodeTags() + .HumanizeMultilineCodeTags(xmlCommentEndOfLine) .HumanizeParaTags() .HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional
tags .DecodeXml(); @@ -33,10 +33,10 @@ public static string Humanize(string text, string xmlCommentEndOfLine) private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine) { - string[] lines = text.Split('\n'); + var lines = text.Split(["\r\n", "\n"], StringSplitOptions.None); string padding = GetCommonLeadingWhitespace(lines); - int padLen = padding == null ? 0 : padding.Length; + int padLen = padding?.Length ?? 0; // remove leading padding from each line for (int i = 0, l = lines.Length; i < l; ++i) @@ -51,7 +51,7 @@ private static string NormalizeIndentation(this string text, string xmlCommentEn // 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(); + return string.Join(EndOfLine(xmlCommentEndOfLine), lines.SkipWhile(string.IsNullOrWhiteSpace)).TrimEnd(); } private static string GetCommonLeadingWhitespace(string[] lines) @@ -105,7 +105,7 @@ private static string HumanizeCodeTags(this string text) return CodeTag().Replace(text, (match) => "`" + match.Groups["display"].Value + "`"); } - private static string HumanizeMultilineCodeTags(this string text) + private static string HumanizeMultilineCodeTags(this string text, string xmlCommentEndOfLine) { return MultilineCodeTag().Replace(text, match => { @@ -115,12 +115,17 @@ private static string HumanizeMultilineCodeTags(this string text) var builder = new StringBuilder().Append("```"); if (!codeText.StartsWith("\r") && !codeText.StartsWith("\n")) { - builder.AppendLine(); + builder.Append(EndOfLine(xmlCommentEndOfLine)); } - return builder.AppendLine(codeText.TrimEnd()) - .Append("```") - .ToString(); + builder.Append(RemoveCommonLeadingWhitespace(codeText, xmlCommentEndOfLine)); + if (!codeText.EndsWith("\n")) + { + builder.Append(EndOfLine(xmlCommentEndOfLine)); + } + + builder.Append("```"); + return DoubleUpLineBreaks().Replace(builder.ToString(), EndOfLine(xmlCommentEndOfLine)); } return $"```{codeText}```"; @@ -129,16 +134,12 @@ private static string HumanizeMultilineCodeTags(this string text) private static string HumanizeParaTags(this string text) { - return ParaTag().Replace(text, match => - { - var paraText = "
" + match.Groups["display"].Value.Trim(); - return LineBreaks().Replace(paraText, _ => string.Empty); - }); + return ParaTag().Replace(text, match => "
" + match.Groups["display"].Value.Trim()); } private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine) { - return BrTag().Replace(text, _ => xmlCommentEndOfLine ?? Environment.NewLine); + return BrTag().Replace(text, _ => EndOfLine(xmlCommentEndOfLine)); } private static string DecodeXml(this string text) @@ -146,6 +147,33 @@ 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 padding = GetCommonLeadingWhitespace(lines); + if (string.IsNullOrEmpty(padding)) + { + return input; + } + + var minLeadingSpaces = padding.Length; + 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(); + } + + internal static string EndOfLine(string xmlCommentEndOfLine) + { + return xmlCommentEndOfLine ?? Environment.NewLine; + } + private const string RefTagPattern = @"<(see|paramref) (name|cref|langword)=""([TPF]{1}:)?(?.+?)"" ?/>"; private const string CodeTagPattern = @"(?.+?)"; private const string MultilineCodeTagPattern = @"(?.+?)"; @@ -153,6 +181,7 @@ private static string DecodeXml(this string text) private const string HrefPattern = @"(.*)<\/see>"; private const string BrPattern = @"(
)"; // handles
,
,
private const string LineBreaksPattern = @"\r?\n"; + private const string DoubleUpLineBreaksPattern = @"(\r?\n){2,}"; #if NET7_0_OR_GREATER [GeneratedRegex(RefTagPattern)] @@ -175,6 +204,9 @@ private static string DecodeXml(this string text) [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); @@ -183,6 +215,7 @@ private static string DecodeXml(this string text) 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; @@ -191,6 +224,7 @@ private static string DecodeXml(this string text) private static Regex HrefTag() => _hrefTag; private static Regex BrTag() => _brTag; private static Regex LineBreaks() => _lineBreaks; + private static Regex DoubleUpLineBreaks() => _doubleUpLineBreaks; #endif } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_DotNet6_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_DotNet6_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt index 9ba7fd7080..360844ea55 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_DotNet6_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_DotNet6_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt @@ -93,7 +93,7 @@ "CrudActions" ], "summary": "Get all products", - "description": "```\r\n {\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n }\r\n```", + "description": "```\r\n{\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n}\r\n \r\n```", "responses": { "200": { "description": "OK", @@ -199,7 +199,7 @@ "CrudActions" ], "summary": "Updates some properties of a specific product", - "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", + "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", "operationId": "PatchProduct", "parameters": [ { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt index d569ed88c2..32e5af36f3 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_startupType=Basic.Startup_swaggerRequestUri=v1.verified.txt @@ -93,7 +93,7 @@ "CrudActions" ], "summary": "Get all products", - "description": "```\r\n {\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n }\r\n```", + "description": "```\r\n{\r\n \"Id\":1,\r\n \"Description\":\"\",\r\n \"Status\": 0,\r\n \"Status2\": 1\r\n}\r\n \r\n```", "responses": { "200": { "description": "OK", @@ -199,7 +199,7 @@ "CrudActions" ], "summary": "Updates some properties of a specific product", - "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", + "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", "operationId": "PatchProduct", "parameters": [ { diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs index cf554a59d1..481c96a79d 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs @@ -1,5 +1,4 @@ -using System; -using Xunit; +using Xunit; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { @@ -145,29 +144,10 @@ public void Humanize_HumanizesInlineTags( Assert.Equal(expectedOutput, output, false, true); } - [Fact] - public void Humanize_MultilineBrTag_EolNotSpecified() - { - const string input = @" - This is a paragraph. -
- A parameter after br tag."; - - var output = XmlCommentsTextHelper.Humanize(input); - - // Result view for Linux: This is a paragraph.\r\n\n\r\nA parameter after br tag. - var expected = string.Join("\r\n", - [ - "This is a paragraph.", - Environment.NewLine, - "A parameter after br tag." - ]); - Assert.Equal(expected, output, false, ignoreLineEndingDifferences: false); - } - [Theory] [InlineData("\r\n")] [InlineData("\n")] + [InlineData(null)] public void Humanize_MultilineBrTag_SpecificEol(string xmlCommentEndOfLine) { const string input = @" @@ -177,7 +157,7 @@ This is a paragraph. var output = XmlCommentsTextHelper.Humanize(input, xmlCommentEndOfLine); - var expected = string.Join(xmlCommentEndOfLine, + var expected = string.Join(XmlCommentsTextHelper.EndOfLine(xmlCommentEndOfLine), [ "This is a paragraph.", "", @@ -199,7 +179,7 @@ This is a paragraph. var output = XmlCommentsTextHelper.Humanize(input); - Assert.Equal("\r\nThis is a paragraph. MultiLined.\r\n\r\nThis is a paragraph.", output, false, true); + Assert.Equal("\r\nThis is a paragraph.\r\n MultiLined.\r\n\r\nThis is a paragraph.", output, false, true); } [Fact] @@ -215,16 +195,16 @@ public void Humanize_CodeMultiLineTag() var output = XmlCommentsTextHelper.Humanize(input); - var expected = string.Join("\r\n", + var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null), [ "```", - " {", - " \"Prop1\":1,", - " \"Prop2\":[]", - " }", + "{", + " \"Prop1\":1,", + " \"Prop2\":[]", + "}", "```" ]); - Assert.Equal(expected, output, false, true); + Assert.Equal(expected, output); } [Fact] @@ -239,7 +219,7 @@ public void Humanize_CodeMultiLineTag_OnSameLine() var output = XmlCommentsTextHelper.Humanize(input); - var expected = string.Join("\r\n", + var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null), [ "```", "{", @@ -248,7 +228,52 @@ public void Humanize_CodeMultiLineTag_OnSameLine() " }", "```" ]); - Assert.Equal(expected, output, false, true); + Assert.Equal(expected, output); + } + + [Fact] + public void Humanize_CodeInsideParaTag() + { + var input = string.Join(XmlCommentsTextHelper.EndOfLine(null), + [ + "Creates a new Answer", + "", + ]); + + var output = XmlCommentsTextHelper.Humanize(input); + + var expected = string.Join(XmlCommentsTextHelper.EndOfLine(null), + [ + "", + "Creates a new Answer", + "", + "```", + "", + "```" + ]); + Assert.Equal(expected, output); } } }