diff --git a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs index 6d7f3f64d..05396a2f5 100755 --- a/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiWriterBase.cs @@ -23,7 +23,7 @@ public abstract class OpenApiWriterBase : IOpenApiWriter /// /// The indentation string to prepand to each line for each indentation level. /// - private const string IndentationString = " "; + protected const string IndentationString = " "; /// /// Scope of the Open API element - object, array, property. diff --git a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs index 2806d3d64..2eed59608 100755 --- a/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs +++ b/src/Microsoft.OpenApi/Writers/OpenApiYamlWriter.cs @@ -27,6 +27,8 @@ public OpenApiYamlWriter(TextWriter textWriter, OpenApiWriterSettings settings) { } + public bool UseLiteralStyle { get; set; } + /// /// Base Indentation Level. /// This denotes how many indentations are needed for the property in the base object. @@ -163,11 +165,105 @@ public override void WritePropertyName(string name) /// The string value. public override void WriteValue(string value) { - WriteValueSeparator(); + if (!UseLiteralStyle || value.IndexOfAny(new [] { '\n', '\r' }) == -1) + { + WriteValueSeparator(); - value = value.GetYamlCompatibleString(); + value = value.GetYamlCompatibleString(); - Writer.Write(value); + Writer.Write(value); + } + else + { + if (CurrentScope() != null) + { + WriteValueSeparator(); + } + + Writer.Write("|"); + + WriteChompingIndicator(value); + + // Write indentation indicator when it starts with spaces + if (value.StartsWith(" ")) + { + Writer.Write(IndentationString.Length); + } + + Writer.WriteLine(); + + IncreaseIndentation(); + + using (var reader = new StringReader(value)) + { + bool firstLine = true; + while (reader.ReadLine() is var line && line != null) + { + if (firstLine) + firstLine = false; + else + Writer.WriteLine(); + + // Indentations for empty lines aren't needed. + if (line.Length > 0) + { + WriteIndentation(); + } + + Writer.Write(line); + } + } + + DecreaseIndentation(); + } + } + + private void WriteChompingIndicator(string value) + { + var trailingNewlines = 0; + var end = value.Length - 1; + // We only need to know whether there are 0, 1, or more trailing newlines + while (end >= 0 && trailingNewlines < 2) + { + var found = value.LastIndexOfAny(new[] { '\n', '\r' }, end, 2); + if (found == -1 || found != end) + { + // does not ends with newline + break; + } + + if (value[end] == '\r') + { + // ends with \r + end--; + } + else if (end > 0 && value[end - 1] == '\r') + { + // ends with \r\n + end -= 2; + } + else + { + // ends with \n + end -= 1; + } + trailingNewlines++; + } + + switch (trailingNewlines) + { + case 0: + // "strip" chomping indicator + Writer.Write("-"); + break; + case 1: + // "clip" + break; + default: + // "keep" chomping indicator + Writer.Write("+"); + break; + } } /// diff --git a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs index 60e598882..78a2c6678 100644 --- a/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Writers/OpenApiWriterSpecialCharacterTests.cs @@ -75,5 +75,59 @@ public void WriteStringWithSpecialCharactersAsYamlWorks(string input, string exp // Assert actual.Should().Be(expected); } + + [Theory] + [InlineData("multiline\r\nstring", "test: |-\n multiline\n string")] + [InlineData("ends with\r\nline break\r\n", "test: |\n ends with\n line break")] + [InlineData("ends with\r\n2 line breaks\r\n\r\n", "test: |+\n ends with\n 2 line breaks\n")] + [InlineData("ends with\r\n3 line breaks\r\n\r\n\r\n", "test: |+\n ends with\n 3 line breaks\n\n")] + [InlineData(" starts with\nspaces", "test: |-2\n starts with\n spaces")] + [InlineData(" starts with\nspaces, and ends with line break\n", "test: |2\n starts with\n spaces, and ends with line break")] + [InlineData("contains\n\n\nempty lines", "test: |-\n contains\n\n\n empty lines")] + [InlineData("no line breaks fallback ", "test: 'no line breaks fallback '")] + public void WriteStringWithNewlineCharactersInObjectAsYamlWorks(string input, string expected) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputStringWriter) { UseLiteralStyle = true, }; + + // Act + writer.WriteStartObject(); + writer.WritePropertyName("test"); + writer.WriteValue(input); + writer.WriteEndObject(); + var actual = outputStringWriter.GetStringBuilder().ToString() + // Normalize newline for cross platform + .Replace("\r", ""); + + // Assert + actual.Should().Be(expected); + } + + [Theory] + [InlineData("multiline\r\nstring", "- |-\n multiline\n string")] + [InlineData("ends with\r\nline break\r\n", "- |\n ends with\n line break")] + [InlineData("ends with\r\n2 line breaks\r\n\r\n", "- |+\n ends with\n 2 line breaks\n")] + [InlineData("ends with\r\n3 line breaks\r\n\r\n\r\n", "- |+\n ends with\n 3 line breaks\n\n")] + [InlineData(" starts with\nspaces", "- |-2\n starts with\n spaces")] + [InlineData(" starts with\nspaces, and ends with line break\n", "- |2\n starts with\n spaces, and ends with line break")] + [InlineData("contains\n\n\nempty lines", "- |-\n contains\n\n\n empty lines")] + [InlineData("no line breaks fallback ", "- 'no line breaks fallback '")] + public void WriteStringWithNewlineCharactersInArrayAsYamlWorks(string input, string expected) + { + // Arrange + var outputStringWriter = new StringWriter(CultureInfo.InvariantCulture); + var writer = new OpenApiYamlWriter(outputStringWriter) { UseLiteralStyle = true, }; + + // Act + writer.WriteStartArray(); + writer.WriteValue(input); + var actual = outputStringWriter.GetStringBuilder().ToString() + // Normalize newline for cross platform + .Replace("\r", ""); + + // Assert + actual.Should().Be(expected); + } } }