diff --git a/src/IndentingBuilder/IndentingBuilder.cs b/src/IndentingBuilder/IndentingBuilder.cs new file mode 100644 index 0000000..40eab79 --- /dev/null +++ b/src/IndentingBuilder/IndentingBuilder.cs @@ -0,0 +1,243 @@ +using System.Runtime.CompilerServices; +using System.Text; + +namespace StaticCs; + +/// +/// A string builder with automatic indentation support for multi-line text. +/// Manages indentation levels with and methods, +/// and automatically applies the current indentation to appended content and interpolated strings. +/// +public sealed class IndentingBuilder : IComparable, IEquatable +{ + public static readonly Encoding UTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + private string _currentIndentWhitespace = ""; + private StringBuilder _stringBuilder; + + public IndentingBuilder(string s) + { + _stringBuilder = new StringBuilder(s); + } + + public IndentingBuilder(SourceBuilderStringHandler s) + { + _currentIndentWhitespace = ""; + _stringBuilder = s._stringBuilder; + } + + public IndentingBuilder() + { + _stringBuilder = new StringBuilder(); + } + + /// + /// Removes trailing whitespace from every line and replace all newlines with + /// Environment.NewLine. + /// + private void Normalize() + { + _stringBuilder.Replace("\r\n", "\n"); + + // Remove trailing whitespace from every line + int wsStart; + for (int i = 0; i < _stringBuilder.Length; i++) + { + if (_stringBuilder[i] is '\n') + { + wsStart = i - 1; + while (wsStart >= 0 && (_stringBuilder[wsStart] is ' ' or '\t')) + { + wsStart--; + } + wsStart++; // Move back to first whitespace + if (wsStart < i) + { + int len = i - wsStart; + _stringBuilder.Remove(wsStart, len); + i -= len; + } + } + } + + _stringBuilder.Replace("\n", Environment.NewLine); + } + + public override string ToString() + { + Normalize(); + return _stringBuilder.ToString(); + } + + public void Append( + [InterpolatedStringHandlerArgument("")] + SourceBuilderStringHandler s) + { + // No work needed, the handler has already added the text to the string builder + } + + public void Append(string s) + { + _stringBuilder.Append(_currentIndentWhitespace); + Append(_stringBuilder, _currentIndentWhitespace, s); + } + + public void Append(IndentingBuilder srcBuilder) + { + Append(srcBuilder.ToString()); + } + + private static void Append( + StringBuilder builder, + string currentIndentWhitespace, + string str) + { + int start = 0; + int nl; + while (start < str.Length) + { + nl = str.IndexOf('\n', start); + if (nl == -1) + { + nl = str.Length; + } + // Skip blank lines + while (nl < str.Length && (str[nl] == '\n' || str[nl] == '\r')) + { + nl++; + } + if (start > 0) + { + builder.Append(currentIndentWhitespace); + } + builder.Append(str, start, nl - start); + start = nl; + } + } + + public void AppendLine( + [InterpolatedStringHandlerArgument("")] + SourceBuilderStringHandler s) + { + Append(s); + _stringBuilder.AppendLine(); + } + + public void AppendLine(string s) + { + Append(s); + _stringBuilder.AppendLine(); + } + + public int CompareTo(IndentingBuilder? other) + { + if (other is null) return 1; + + var lenCmp = _stringBuilder.Length.CompareTo(other._stringBuilder.Length); + if (lenCmp != 0) + { + return lenCmp; + } + for (int i = 0; i < _stringBuilder.Length; i++) + { + var cCmp = _stringBuilder[i].CompareTo(other._stringBuilder[i]); + if (cCmp != 0) + { + return cCmp; + } + } + return 0; + } + + public void Indent() + { + _currentIndentWhitespace += " "; + } + + public void Dedent() + { + _currentIndentWhitespace = _currentIndentWhitespace[..^4]; + } + + public bool Equals(IndentingBuilder? other) + { + return _stringBuilder.Equals(other?._stringBuilder); + } + + public void AppendLine(IndentingBuilder deserialize) + { + Append(deserialize); + _stringBuilder.AppendLine(); + } + + [InterpolatedStringHandler] + public ref struct SourceBuilderStringHandler + { + internal readonly StringBuilder _stringBuilder; + private readonly string _originalIndentWhitespace; + private string _currentIndentWhitespace; + private bool _isFirst = true; + + public SourceBuilderStringHandler(int literalLength, int formattedCount) + { + _stringBuilder = new StringBuilder(literalLength); + _originalIndentWhitespace = ""; + _currentIndentWhitespace = ""; + } + + public SourceBuilderStringHandler( + int literalLength, + int formattedCount, + IndentingBuilder sourceBuilder) + { + _stringBuilder = sourceBuilder._stringBuilder; + _originalIndentWhitespace = sourceBuilder._currentIndentWhitespace; + _currentIndentWhitespace = sourceBuilder._currentIndentWhitespace; + } + + public void AppendLiteral(string s) + { + if (_isFirst) + { + _stringBuilder.Append(_currentIndentWhitespace); + _isFirst = false; + } + Append(_stringBuilder, _currentIndentWhitespace, s); + + int last = s.LastIndexOf('\n'); + if (last == -1) + { + return; + } + + var remaining = s.AsSpan(last + 1); + foreach (var c in remaining) + { + if (c is not (' ' or '\t')) + { + return; + } + } + + _currentIndentWhitespace += remaining.ToString(); + } + + public void AppendFormatted(T value) + { + if (_isFirst) + { + _stringBuilder.Append(_currentIndentWhitespace); + _isFirst = false; + } + var str = value?.ToString(); + if (str is null) + { + _stringBuilder.Append(str); + return; + } + + Append(_stringBuilder, _currentIndentWhitespace, str); + _currentIndentWhitespace = _originalIndentWhitespace; + } + } +} \ No newline at end of file diff --git a/src/IndentingBuilder/IndentingBuilder.csproj b/src/IndentingBuilder/IndentingBuilder.csproj new file mode 100644 index 0000000..30bc856 --- /dev/null +++ b/src/IndentingBuilder/IndentingBuilder.csproj @@ -0,0 +1,29 @@ + + + + net8.0;netstandard2.0 + enable + enable + 12.0 + + + + StaticCS.IndentingBuilder + 0.1.0 + true + MIT + https://github.com/agocke/static-cs + A string builder with automatic indentation support for multi-line text generation. + agocke + string-builder;indentation;code-generation;text-generation + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/static-cs.sln b/static-cs.sln index bed3c64..c9c99d9 100644 --- a/static-cs.sln +++ b/static-cs.sln @@ -1,71 +1,146 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E383093D-99E0-4B3D-8B3B-C960B7B7C426}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Collections", "src\Collections\StaticCs.Collections.csproj", "{44789D3C-044A-457B-9F5F-47209D0D5C21}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Async", "src\Async\StaticCs.Async.csproj", "{48E1C9BD-5F90-4028-95E0-A4EA00201F15}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9E2044F-C0D2-4270-A896-E9917D1331D0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test\test.csproj", "{2D168E49-8396-43D4-BF8F-8CF0796CF2DD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AotTest", "test\AotTest\AotTest.csproj", "{7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs", "src\StaticCs\StaticCs.csproj", "{FD238201-2E9D-4066-83BB-43D3DD49EC24}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.TrimmableConverter", "src\StaticCs.TrimmableConverter\StaticCs.TrimmableConverter.csproj", "{B29FFDB1-7006-42FA-9647-3101CDFB364F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Result", "src\Result\StaticCs.Result.csproj", "{8FF14261-BA9C-487F-8581-3F7D978EA772}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|Any CPU.Build.0 = Debug|Any CPU - {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|Any CPU.ActiveCfg = Release|Any CPU - {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|Any CPU.Build.0 = Release|Any CPU - {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|Any CPU.Build.0 = Release|Any CPU - {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|Any CPU.Build.0 = Release|Any CPU - {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|Any CPU.Build.0 = Release|Any CPU - {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|Any CPU.Build.0 = Release|Any CPU - {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|Any CPU.Build.0 = Release|Any CPU - {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {44789D3C-044A-457B-9F5F-47209D0D5C21} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} - {48E1C9BD-5F90-4028-95E0-A4EA00201F15} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} - {2D168E49-8396-43D4-BF8F-8CF0796CF2DD} = {E9E2044F-C0D2-4270-A896-E9917D1331D0} - {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA} = {E9E2044F-C0D2-4270-A896-E9917D1331D0} - {FD238201-2E9D-4066-83BB-43D3DD49EC24} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} - {B29FFDB1-7006-42FA-9647-3101CDFB364F} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} - {8FF14261-BA9C-487F-8581-3F7D978EA772} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E383093D-99E0-4B3D-8B3B-C960B7B7C426}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Collections", "src\Collections\StaticCs.Collections.csproj", "{44789D3C-044A-457B-9F5F-47209D0D5C21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Async", "src\Async\StaticCs.Async.csproj", "{48E1C9BD-5F90-4028-95E0-A4EA00201F15}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E9E2044F-C0D2-4270-A896-E9917D1331D0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "test", "test\test\test.csproj", "{2D168E49-8396-43D4-BF8F-8CF0796CF2DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AotTest", "test\AotTest\AotTest.csproj", "{7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs", "src\StaticCs\StaticCs.csproj", "{FD238201-2E9D-4066-83BB-43D3DD49EC24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.TrimmableConverter", "src\StaticCs.TrimmableConverter\StaticCs.TrimmableConverter.csproj", "{B29FFDB1-7006-42FA-9647-3101CDFB364F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StaticCs.Result", "src\Result\StaticCs.Result.csproj", "{8FF14261-BA9C-487F-8581-3F7D978EA772}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IndentingBuilder", "src\IndentingBuilder\IndentingBuilder.csproj", "{3EBB37BC-0309-4562-9785-0CC2D9D23944}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|x64.ActiveCfg = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|x64.Build.0 = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|x86.ActiveCfg = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Debug|x86.Build.0 = Debug|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|Any CPU.Build.0 = Release|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|x64.ActiveCfg = Release|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|x64.Build.0 = Release|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|x86.ActiveCfg = Release|Any CPU + {44789D3C-044A-457B-9F5F-47209D0D5C21}.Release|x86.Build.0 = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|x64.ActiveCfg = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|x64.Build.0 = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|x86.ActiveCfg = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Debug|x86.Build.0 = Debug|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|Any CPU.Build.0 = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|x64.ActiveCfg = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|x64.Build.0 = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|x86.ActiveCfg = Release|Any CPU + {48E1C9BD-5F90-4028-95E0-A4EA00201F15}.Release|x86.Build.0 = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|x64.Build.0 = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Debug|x86.Build.0 = Debug|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|Any CPU.Build.0 = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|x64.ActiveCfg = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|x64.Build.0 = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|x86.ActiveCfg = Release|Any CPU + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD}.Release|x86.Build.0 = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|x64.Build.0 = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Debug|x86.Build.0 = Debug|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|Any CPU.Build.0 = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|x64.ActiveCfg = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|x64.Build.0 = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|x86.ActiveCfg = Release|Any CPU + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA}.Release|x86.Build.0 = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|x64.Build.0 = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Debug|x86.Build.0 = Debug|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|Any CPU.Build.0 = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|x64.ActiveCfg = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|x64.Build.0 = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|x86.ActiveCfg = Release|Any CPU + {FD238201-2E9D-4066-83BB-43D3DD49EC24}.Release|x86.Build.0 = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|x64.ActiveCfg = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|x64.Build.0 = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|x86.ActiveCfg = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Debug|x86.Build.0 = Debug|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|Any CPU.Build.0 = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|x64.ActiveCfg = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|x64.Build.0 = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|x86.ActiveCfg = Release|Any CPU + {B29FFDB1-7006-42FA-9647-3101CDFB364F}.Release|x86.Build.0 = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|x64.ActiveCfg = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|x64.Build.0 = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|x86.ActiveCfg = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Debug|x86.Build.0 = Debug|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|Any CPU.Build.0 = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|x64.ActiveCfg = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|x64.Build.0 = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|x86.ActiveCfg = Release|Any CPU + {8FF14261-BA9C-487F-8581-3F7D978EA772}.Release|x86.Build.0 = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|x64.Build.0 = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Debug|x86.Build.0 = Debug|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|Any CPU.Build.0 = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|x64.ActiveCfg = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|x64.Build.0 = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|x86.ActiveCfg = Release|Any CPU + {3EBB37BC-0309-4562-9785-0CC2D9D23944}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {44789D3C-044A-457B-9F5F-47209D0D5C21} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + {48E1C9BD-5F90-4028-95E0-A4EA00201F15} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + {2D168E49-8396-43D4-BF8F-8CF0796CF2DD} = {E9E2044F-C0D2-4270-A896-E9917D1331D0} + {7FD737C0-6ED4-44E9-BC40-096BFD16DBDA} = {E9E2044F-C0D2-4270-A896-E9917D1331D0} + {FD238201-2E9D-4066-83BB-43D3DD49EC24} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + {B29FFDB1-7006-42FA-9647-3101CDFB364F} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + {8FF14261-BA9C-487F-8581-3F7D978EA772} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + {3EBB37BC-0309-4562-9785-0CC2D9D23944} = {E383093D-99E0-4B3D-8B3B-C960B7B7C426} + EndGlobalSection +EndGlobal diff --git a/test/test/IndentingBuilderTests.cs b/test/test/IndentingBuilderTests.cs new file mode 100644 index 0000000..2e83fba --- /dev/null +++ b/test/test/IndentingBuilderTests.cs @@ -0,0 +1,412 @@ +using System; +using System.Text; +using Xunit; + +namespace StaticCs.Tests; + +public sealed class IndentingBuilderTests +{ + [Fact] + public void Constructor_Default_CreatesEmptyBuilder() + { + var builder = new IndentingBuilder(); + Assert.Equal("", builder.ToString()); + } + + [Fact] + public void Constructor_WithString_InitializesWithContent() + { + var builder = new IndentingBuilder("Hello, World!"); + Assert.Equal("Hello, World!", builder.ToString()); + } + + [Fact] + public void Constructor_WithInterpolatedString_InitializesWithContent() + { + var name = "World"; + var builder = new IndentingBuilder($"Hello, {name}!"); + Assert.Equal("Hello, World!", builder.ToString()); + } + + [Fact] + public void Append_SimpleString_AppendsContent() + { + var builder = new IndentingBuilder(); + builder.Append("Hello"); + Assert.Equal("Hello", builder.ToString()); + } + + [Fact] + public void AppendLine_SimpleString_AppendsWithNewline() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Hello"); + var expected = "Hello" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void AppendLine_MultipleLines_PreservesNewlines() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Line 1"); + builder.AppendLine("Line 2"); + var expected = "Line 1" + Environment.NewLine + "Line 2" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Append_IndentingBuilder_AppendsContent() + { + var builder1 = new IndentingBuilder(); + builder1.Append("Hello"); + + var builder2 = new IndentingBuilder(); + builder2.Append(builder1); + + Assert.Equal("Hello", builder2.ToString()); + } + + [Fact] + public void AppendLine_WithIndentingBuilder_AppendsWithNewline() + { + var builder1 = new IndentingBuilder(); + builder1.Append("Inner content"); + + var builder2 = new IndentingBuilder(); + builder2.AppendLine(builder1); + builder2.Append("Next line"); + + var expected = "Inner content" + Environment.NewLine + "Next line"; + Assert.Equal(expected, builder2.ToString()); + } + + [Fact] + public void Indent_IncreasesIndentation() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Level 0"); + builder.Indent(); + builder.AppendLine("Level 1"); + builder.Indent(); + builder.AppendLine("Level 2"); + + var expected = "Level 0" + Environment.NewLine + + " Level 1" + Environment.NewLine + + " Level 2" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Dedent_DecreasesIndentation() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Level 0"); + builder.Indent(); + builder.AppendLine("Level 1"); + builder.Dedent(); + builder.AppendLine("Back to Level 0"); + + var expected = "Level 0" + Environment.NewLine + + " Level 1" + Environment.NewLine + + "Back to Level 0" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Indent_Dedent_MultipleLevel_WorksCorrectly() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Level 0"); + builder.Indent(); + builder.AppendLine("Level 1"); + builder.Indent(); + builder.AppendLine("Level 2"); + builder.Indent(); + builder.AppendLine("Level 3"); + builder.Dedent(); + builder.Dedent(); + builder.AppendLine("Level 1"); + builder.Dedent(); + builder.AppendLine("Level 0"); + + var expected = "Level 0" + Environment.NewLine + + " Level 1" + Environment.NewLine + + " Level 2" + Environment.NewLine + + " Level 3" + Environment.NewLine + + " Level 1" + Environment.NewLine + + "Level 0" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Append_StringWithNewlines_PreservesIndentation() + { + var builder = new IndentingBuilder(); + builder.Indent(); + builder.Append("Line 1\nLine 2\nLine 3"); + + var expected = " Line 1" + Environment.NewLine + + " Line 2" + Environment.NewLine + + " Line 3"; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Normalize_RemovesTrailingWhitespace() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Hello "); + builder.AppendLine("World\t\t"); + + var expected = "Hello" + Environment.NewLine + "World" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Normalize_ConvertsAllNewlinesToEnvironmentNewline() + { + var builder = new IndentingBuilder("Line1\nLine2\r\nLine3"); + var result = builder.ToString(); + + Assert.Contains("Line1" + Environment.NewLine, result); + Assert.Contains("Line2" + Environment.NewLine, result); + Assert.Contains("Line3", result); + } + + [Fact] + public void CompareTo_EqualBuilders_ReturnsZero() + { + var builder1 = new IndentingBuilder("Test"); + var builder2 = new IndentingBuilder("Test"); + + Assert.Equal(0, builder1.CompareTo(builder2)); + } + + [Fact] + public void CompareTo_DifferentLengths_ReturnsNonZero() + { + var builder1 = new IndentingBuilder("Short"); + var builder2 = new IndentingBuilder("Longer String"); + + Assert.True(builder1.CompareTo(builder2) < 0); + Assert.True(builder2.CompareTo(builder1) > 0); + } + + [Fact] + public void CompareTo_DifferentContent_ReturnsNonZero() + { + var builder1 = new IndentingBuilder("Apple"); + var builder2 = new IndentingBuilder("Banana"); + + Assert.True(builder1.CompareTo(builder2) < 0); + Assert.True(builder2.CompareTo(builder1) > 0); + } + + [Fact] + public void CompareTo_WithNull_ReturnsPositive() + { + var builder = new IndentingBuilder("Test"); + Assert.True(builder.CompareTo(null) > 0); + } + + [Fact] + public void Equals_SameContent_ReturnsTrue() + { + var builder1 = new IndentingBuilder("Test"); + var builder2 = new IndentingBuilder("Test"); + + Assert.True(builder1.Equals(builder2)); + } + + [Fact] + public void Equals_DifferentContent_ReturnsFalse() + { + var builder1 = new IndentingBuilder("Test1"); + var builder2 = new IndentingBuilder("Test2"); + + Assert.False(builder1.Equals(builder2)); + } + + [Fact] + public void Equals_WithNull_ReturnsFalse() + { + var builder = new IndentingBuilder("Test"); + Assert.False(builder.Equals(null)); + } + + [Fact] + public void AppendLine_WithIndentingBuilder_WorksCorrectly() + { + var builder1 = new IndentingBuilder(); + builder1.Append("Inner content"); + + var builder2 = new IndentingBuilder(); + builder2.AppendLine(builder1); + + var expected = "Inner content" + Environment.NewLine; + Assert.Equal(expected, builder2.ToString()); + } + + [Fact] + public void AppendLine_WithIndentingBuilder_PreservesIndentation() + { + var builder1 = new IndentingBuilder(); + builder1.Append("First"); + builder1.AppendLine("Second"); + + var builder2 = new IndentingBuilder(); + builder2.Indent(); + builder2.AppendLine(builder1); + builder2.Append("After"); + + var expected = " FirstSecond" + Environment.NewLine + + Environment.NewLine + + " After"; + Assert.Equal(expected, builder2.ToString()); + } + + [Fact] + public void InterpolatedString_SimpleValues_WorksCorrectly() + { + var builder = new IndentingBuilder(); + var name = "World"; + var number = 42; + builder.Append($"Hello {name}, the answer is {number}"); + + Assert.Equal("Hello World, the answer is 42", builder.ToString()); + } + + [Fact] + public void InterpolatedString_StartingWithValue_AppliesIndentation() + { + var builder = new IndentingBuilder(); + builder.Indent(); + var value = "test"; + builder.Append($"{value} is the value"); + + Assert.Equal(" test is the value", builder.ToString()); + } + + [Fact] + public void InterpolatedString_WithIndentation_WorksCorrectly() + { + var builder = new IndentingBuilder(); + builder.Indent(); + var value = "test"; + builder.AppendLine($"Value: {value}"); + + var expected = " Value: test" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void InterpolatedString_WithMultilineContent_PreservesIndentation() + { + var builder = new IndentingBuilder(); + builder.Indent(); + var multiline = "Line1\nLine2"; + builder.Append($"Start\n{multiline}\nEnd"); + + // After a formatted value, indentation resets to the original level + var expected = " Start" + Environment.NewLine + + "Line1" + Environment.NewLine + + " Line2" + Environment.NewLine + + " End"; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void EmptyBuilder_ToString_ReturnsEmptyString() + { + var builder = new IndentingBuilder(); + Assert.Equal("", builder.ToString()); + } + + [Fact] + public void ComplexScenario_MixedIndentationAndContent_WorksCorrectly() + { + var builder = new IndentingBuilder(); + builder.AppendLine("public class Example"); + builder.AppendLine("{"); + builder.Indent(); + builder.AppendLine("public void Method()"); + builder.AppendLine("{"); + builder.Indent(); + builder.AppendLine("Console.WriteLine(\"Hello\");"); + builder.AppendLine("Console.WriteLine(\"World\");"); + builder.Dedent(); + builder.AppendLine("}"); + builder.Dedent(); + builder.AppendLine("}"); + + var expected = "public class Example" + Environment.NewLine + + "{" + Environment.NewLine + + " public void Method()" + Environment.NewLine + + " {" + Environment.NewLine + + " Console.WriteLine(\"Hello\");" + Environment.NewLine + + " Console.WriteLine(\"World\");" + Environment.NewLine + + " }" + Environment.NewLine + + "}" + Environment.NewLine; + Assert.Equal(expected, builder.ToString()); + } + + [Fact] + public void Append_BlankLines_PreservesBlankLines() + { + var builder = new IndentingBuilder(); + builder.Indent(); + builder.Append("Line 1\n\nLine 3"); + + var result = builder.ToString(); + Assert.Contains("Line 1" + Environment.NewLine, result); + Assert.Contains("Line 3", result); + } + + [Fact] + public void ToString_CalledMultipleTimes_ReturnsConsistentResults() + { + var builder = new IndentingBuilder(); + builder.AppendLine("Test "); + + var result1 = builder.ToString(); + var result2 = builder.ToString(); + + Assert.Equal(result1, result2); + Assert.Equal("Test" + Environment.NewLine, result1); + } + + [Fact] + public void InterpolatedString_WithNullValue_HandlesGracefully() + { + var builder = new IndentingBuilder(); + string? nullValue = null; + builder.Append($"Value: {nullValue}"); + + Assert.Equal("Value: ", builder.ToString()); + } + + [Fact] + public void MultipleIndents_ThenDedents_MaintainsCorrectLevel() + { + var builder = new IndentingBuilder(); + builder.AppendLine("L0"); + + for (int i = 1; i <= 5; i++) + { + builder.Indent(); + builder.AppendLine($"L{i}"); + } + + for (int i = 4; i >= 0; i--) + { + builder.Dedent(); + builder.AppendLine($"Back to L{i}"); + } + + var result = builder.ToString(); + Assert.StartsWith("L0" + Environment.NewLine, result); + Assert.Contains(" L5" + Environment.NewLine, result); // 20 spaces (5 * 4) + Assert.EndsWith("Back to L0" + Environment.NewLine, result); + } +} diff --git a/test/test/test.csproj b/test/test/test.csproj index 27c59a1..9aefe46 100644 --- a/test/test/test.csproj +++ b/test/test/test.csproj @@ -7,6 +7,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + @@ -21,6 +25,7 @@ +