Skip to content

Optional EOL for XmlComments (#2947) #3255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions tar
target.RequestBodyAsyncFilters = new List<IRequestBodyAsyncFilter>(source.RequestBodyAsyncFilters);
target.SecuritySchemesSelector = source.SecuritySchemesSelector;
target.PathGroupSelector = source.PathGroupSelector;
target.XmlCommentEndOfLine = source.XmlCommentEndOfLine;
}

private TFilter GetOrCreateFilter<TFilter>(FilterDescriptor filterDescriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,10 +706,10 @@ public static void IncludeXmlComments(
var xmlDoc = xmlDocFactory();
var xmlDocMembers = XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc);

swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers));
swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers));
swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers));
swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers));
swaggerGenOptions.AddParameterFilterInstance(new XmlCommentsParameterFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
swaggerGenOptions.AddRequestBodyFilterInstance(new XmlCommentsRequestBodyFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
swaggerGenOptions.AddOperationFilterInstance(new XmlCommentsOperationFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
swaggerGenOptions.AddSchemaFilterInstance(new XmlCommentsSchemaFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));

if (includeControllerXmlComments)
swaggerGenOptions.AddDocumentFilterInstance(new XmlCommentsDocumentFilter(xmlDocMembers, swaggerGenOptions.SwaggerGeneratorOptions));
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
static Swashbuckle.AspNetCore.SwaggerGen.OpenApiAnyFactory.CreateFromJson(string json, System.Text.Json.JsonSerializerOptions options) -> Microsoft.OpenApi.Any.IOpenApiAny
static Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsTextHelper.Humanize(string text, string xmlCommentEndOfLine) -> string
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.get -> string
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ public SwaggerGeneratorOptions()

public IList<IDocumentAsyncFilter> DocumentAsyncFilters { get; set; }

public string XmlCommentEndOfLine { get; set; }

private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription)
{
return apiDescription.GroupName == null || apiDescription.GroupName == documentName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
swaggerDoc.Tags.Add(new OpenApiTag
{
Name = nameAndType.Key,
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml)
Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Reflection;
Expand All @@ -9,14 +10,17 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
public class XmlCommentsOperationFilter : IOperationFilter
{
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
private readonly SwaggerGeneratorOptions _options;

public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc))
public XmlCommentsOperationFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null)
{
}

internal XmlCommentsOperationFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
[ActivatorUtilitiesConstructor]
internal XmlCommentsOperationFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
{
_xmlDocMembers = xmlDocMembers;
_options = options;
}

public void Apply(OpenApiOperation operation, OperationFilterContext context)
Expand Down Expand Up @@ -52,11 +56,11 @@ private void ApplyMethodTags(OpenApiOperation operation, MethodInfo methodInfo)

var summaryNode = methodNode.SelectFirstChild("summary");
if (summaryNode != null)
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
operation.Summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine);

var remarksNode = methodNode.SelectFirstChild("remarks");
if (remarksNode != null)
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml);
operation.Description = XmlCommentsTextHelper.Humanize(remarksNode.InnerXml, _options?.XmlCommentEndOfLine);

var responseNodes = methodNode.SelectChildren("response");
ApplyResponseTags(operation, responseNodes);
Expand All @@ -73,7 +77,7 @@ private void ApplyResponseTags(OpenApiOperation operation, XPathNodeIterator res
operation.Responses[code] = response;
}

response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml);
response.Description = XmlCommentsTextHelper.Humanize(responseNodes.Current.InnerXml, _options?.XmlCommentEndOfLine);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System.Reflection;
using System.Xml.XPath;
Expand All @@ -8,14 +9,17 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
public class XmlCommentsParameterFilter : IParameterFilter
{
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
private readonly SwaggerGeneratorOptions _options;

public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc))
public XmlCommentsParameterFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null)
{
}

internal XmlCommentsParameterFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
[ActivatorUtilitiesConstructor]
internal XmlCommentsParameterFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
{
_xmlDocMembers = xmlDocMembers;
_options = options;
}

public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
Expand All @@ -39,7 +43,7 @@ private void ApplyPropertyTags(OpenApiParameter parameter, ParameterFilterContex
var summaryNode = propertyNode.SelectFirstChild("summary");
if (summaryNode != null)
{
parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine);
parameter.Schema.Description = null; // no need to duplicate
}

Expand Down Expand Up @@ -68,7 +72,7 @@ private void ApplyParamTags(OpenApiParameter parameter, ParameterFilterContext c

if (paramNode != null)
{
parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);
parameter.Description = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine);

var example = paramNode.GetAttribute("example");
if (string.IsNullOrEmpty(example)) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@
using System.Linq;
using System.Reflection;
using System.Xml.XPath;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class XmlCommentsRequestBodyFilter : IRequestBodyFilter
{
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
private readonly SwaggerGeneratorOptions _options;

public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc))
public XmlCommentsRequestBodyFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null)
{
}

internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
[ActivatorUtilitiesConstructor]
internal XmlCommentsRequestBodyFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
{
_xmlDocMembers = xmlDocMembers;
_options = options;
}

public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context)
Expand Down Expand Up @@ -89,7 +93,7 @@ public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext conte
var summaryNode = propertyNode.SelectFirstChild("summary");
if (summaryNode is not null)
{
summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
summary = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine);
}

var exampleNode = propertyNode.SelectFirstChild("example");
Expand Down Expand Up @@ -147,7 +151,7 @@ private void ApplyPropertyTagsForBody(OpenApiRequestBody requestBody, RequestBod
return (null, null);
}

var summary = XmlCommentsTextHelper.Humanize(paramNode.InnerXml);
var summary = XmlCommentsTextHelper.Humanize(paramNode.InnerXml, _options?.XmlCommentEndOfLine);
var example = paramNode.GetAttribute("example");

return (summary, example);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Xml.XPath;
Expand All @@ -8,14 +9,17 @@ namespace Swashbuckle.AspNetCore.SwaggerGen
public class XmlCommentsSchemaFilter : ISchemaFilter
{
private readonly IReadOnlyDictionary<string, XPathNavigator> _xmlDocMembers;
private readonly SwaggerGeneratorOptions _options;

public XmlCommentsSchemaFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc))
public XmlCommentsSchemaFilter(XPathDocument xmlDoc) : this(XmlCommentsDocumentHelper.CreateMemberDictionary(xmlDoc), null)
{
}

internal XmlCommentsSchemaFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers)
[ActivatorUtilitiesConstructor]
internal XmlCommentsSchemaFilter(IReadOnlyDictionary<string, XPathNavigator> xmlDocMembers, SwaggerGeneratorOptions options)
{
_xmlDocMembers = xmlDocMembers;
_options = options;
}

public void Apply(OpenApiSchema schema, SchemaFilterContext context)
Expand All @@ -38,7 +42,7 @@ private void ApplyTypeTags(OpenApiSchema schema, Type type)

if (typeSummaryNode != null)
{
schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml);
schema.Description = XmlCommentsTextHelper.Humanize(typeSummaryNode.InnerXml, _options?.XmlCommentEndOfLine);
}
}

Expand All @@ -56,7 +60,9 @@ private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context)
{
var summaryNode = recordDefaultConstructorProperty.Value;
if (summaryNode != null)
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode);
{
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode, _options?.XmlCommentEndOfLine);
}

var example = recordDefaultConstructorProperty.GetAttribute("example");
if (!string.IsNullOrEmpty(example))
Expand All @@ -70,7 +76,9 @@ private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context)
{
var summaryNode = fieldOrPropertyNode.SelectFirstChild("summary");
if (summaryNode != null)
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml);
{
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml, _options?.XmlCommentEndOfLine);
}

var exampleNode = fieldOrPropertyNode.SelectFirstChild("example");
TrySetExample(schema, context, exampleNode?.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,29 @@ 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 &lt and &gt to break valid xml

return text
.NormalizeIndentation()
.NormalizeIndentation(xmlCommentEndOfLine)
.HumanizeRefTags()
.HumanizeHrefTags()
.HumanizeCodeTags()
.HumanizeMultilineCodeTags()
.HumanizeParaTags()
.HumanizeBrTags() // must be called after HumanizeParaTags() so that it replaces any additional <br> tags
.HumanizeBrTags(xmlCommentEndOfLine) // must be called after HumanizeParaTags() so that it replaces any additional <br> tags
.DecodeXml();
}

private static string NormalizeIndentation(this string text)
private static string NormalizeIndentation(this string text, string xmlCommentEndOfLine)
{
string[] lines = text.Split('\n');
string padding = GetCommonLeadingWhitespace(lines);
Expand All @@ -46,7 +51,7 @@ private static string NormalizeIndentation(this string text)

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

private static string GetCommonLeadingWhitespace(string[] lines)
Expand Down Expand Up @@ -131,9 +136,9 @@ private static string HumanizeParaTags(this string text)
});
}

private static string HumanizeBrTags(this string text)
private static string HumanizeBrTags(this string text, string xmlCommentEndOfLine)
{
return BrTag().Replace(text, _ => Environment.NewLine);
return BrTag().Replace(text, _ => xmlCommentEndOfLine ?? Environment.NewLine);
}

private static string DecodeXml(this string text)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static void DeepCopy_Copies_All_Properties()

// If this assertion fails, it means that a new property has been added
// to SwaggerGeneratorOptions and ConfigureSwaggerGeneratorOptions.DeepCopy() needs to be updated
Assert.Equal(23, publicProperties.Length);
Assert.Equal(24, publicProperties.Length);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Xunit;
using System;
using Xunit;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test
{
Expand Down Expand Up @@ -144,6 +145,48 @@ public void Humanize_HumanizesInlineTags(
Assert.Equal(expectedOutput, output, false, true);
}

[Fact]
public void Humanize_MultilineBrTag_EolNotSpecified()
{
const string input = @"
This is a paragraph.
<br>
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")]
public void Humanize_MultilineBrTag_SpecificEol(string xmlCommentEndOfLine)
{
const string input = @"
This is a paragraph.
<br>
A parameter after br tag.";

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

var expected = string.Join(xmlCommentEndOfLine,
[
"This is a paragraph.",
"",
"",
"A parameter after br tag."
]);
Assert.Equal(expected, output, false, ignoreLineEndingDifferences: false);
}

[Fact]
public void Humanize_ParaMultiLineTags()
{
Expand Down