diff --git a/Extensions.Test/StringExtensionsTests.cs b/Extensions.Test/StringExtensionsTests.cs index e5d6c95..fbc7678 100644 --- a/Extensions.Test/StringExtensionsTests.cs +++ b/Extensions.Test/StringExtensionsTests.cs @@ -304,6 +304,34 @@ public void NormalizeLineEndingsToNone() Assert.AreEqual(expected, result); } + // A CRLF followed by an LF, and a CR followed by a CRLF, are two line breaks each. Normalizing in + // several passes used to merge the first into a new CRLF under Mac, losing a break and leaving a + // Windows ending in Mac output. + + [TestMethod] + [DataRow("a\r\n\nb", LineEndingStyle.Unix, "a\n\nb")] + [DataRow("a\r\n\nb", LineEndingStyle.Windows, "a\r\n\r\nb")] + [DataRow("a\r\n\nb", LineEndingStyle.Mac, "a\r\rb")] + [DataRow("a\r\n\nb", LineEndingStyle.None, "ab")] + [DataRow("a\r\n\nb", LineEndingStyle.Mixed, "a\n\nb")] + [DataRow("a\r\r\nb", LineEndingStyle.Unix, "a\n\nb")] + [DataRow("a\r\r\nb", LineEndingStyle.Windows, "a\r\n\r\nb")] + [DataRow("a\r\r\nb", LineEndingStyle.Mac, "a\r\rb")] + [DataRow("a\r\r\nb", LineEndingStyle.None, "ab")] + [DataRow("a\r\r\nb", LineEndingStyle.Mixed, "a\n\nb")] + public void NormalizeLineEndingsKeepsEveryBreakOfAdjacentMixedEndings(string input, LineEndingStyle style, string expected) => + Assert.AreEqual(expected, input.NormalizeLineEndings(style)); + + [TestMethod] + [DataRow(LineEndingStyle.Unix)] + [DataRow(LineEndingStyle.Windows)] + [DataRow(LineEndingStyle.Mac)] + public void NormalizeLineEndingsOfAdjacentMixedEndingsIsDetectedAsTheTargetStyle(LineEndingStyle style) + { + Assert.AreEqual(style, "a\r\n\nb".NormalizeLineEndings(style).DetermineLineEndings()); + Assert.AreEqual(style, "a\r\r\nb".NormalizeLineEndings(style).DetermineLineEndings()); + } + [TestMethod] public void NormalizeLineEndingsToMixed() { diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs index 417919d..f1ea5b2 100644 --- a/Extensions/StringExtensions.cs +++ b/Extensions/StringExtensions.cs @@ -126,6 +126,10 @@ public static string ReplaceOrdinal(this string s, string oldValue, string newVa private static Regex LineEndingRegexWindows { get; } = new(@"\r\n", RegexOptions.Compiled); private static Regex LineEndingRegexMac { get; } = new(@"\r(?!\n)", RegexOptions.Compiled); + // One pass over every kind of line ending. Normalizing in several passes lets an earlier pass + // build a sequence the next one reads differently: under Mac, "\r\n\n" became "\r" + "\n", a new CRLF. + private static Regex AnyLineEndingRegex { get; } = new(@"\r\n|\r|\n", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); + /// /// Determines the line ending style of the specified string. /// @@ -202,11 +206,11 @@ public static string NormalizeLineEndings(this string s, LineEndingStyle style) return style switch { - LineEndingStyle.None => LineEndingRegexUnix.Replace(LineEndingRegexWindows.Replace(LineEndingRegexMac.Replace(s, ""), ""), ""), - LineEndingStyle.Unix => LineEndingRegexWindows.Replace(LineEndingRegexMac.Replace(s, "\n"), "\n"), - LineEndingStyle.Windows => LineEndingRegexUnix.Replace(LineEndingRegexMac.Replace(s, "\r\n"), "\r\n"), - LineEndingStyle.Mac => LineEndingRegexUnix.Replace(LineEndingRegexWindows.Replace(s, "\r"), "\r"), - LineEndingStyle.Mixed => LineEndingRegexWindows.Replace(LineEndingRegexMac.Replace(s, "\n"), "\n"), + LineEndingStyle.None => AnyLineEndingRegex.Replace(s, ""), + LineEndingStyle.Unix => AnyLineEndingRegex.Replace(s, "\n"), + LineEndingStyle.Windows => AnyLineEndingRegex.Replace(s, "\r\n"), + LineEndingStyle.Mac => AnyLineEndingRegex.Replace(s, "\r"), + LineEndingStyle.Mixed => AnyLineEndingRegex.Replace(s, "\n"), _ => throw new NotImplementedException("Unknown line ending style."), }; }