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 @@
+