Skip to content

Commit e14a677

Browse files
authored
Add IndentingBuilder for formatting source code (#26)
1 parent e3265d8 commit e14a677

File tree

5 files changed

+835
-71
lines changed

5 files changed

+835
-71
lines changed
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
using System.Runtime.CompilerServices;
2+
using System.Text;
3+
4+
namespace StaticCs;
5+
6+
/// <summary>
7+
/// A string builder with automatic indentation support for multi-line text.
8+
/// Manages indentation levels with <see cref="Indent"/> and <see cref="Dedent"/> methods,
9+
/// and automatically applies the current indentation to appended content and interpolated strings.
10+
/// </summary>
11+
public sealed class IndentingBuilder : IComparable<IndentingBuilder>, IEquatable<IndentingBuilder>
12+
{
13+
public static readonly Encoding UTF8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
14+
15+
private string _currentIndentWhitespace = "";
16+
private StringBuilder _stringBuilder;
17+
18+
public IndentingBuilder(string s)
19+
{
20+
_stringBuilder = new StringBuilder(s);
21+
}
22+
23+
public IndentingBuilder(SourceBuilderStringHandler s)
24+
{
25+
_currentIndentWhitespace = "";
26+
_stringBuilder = s._stringBuilder;
27+
}
28+
29+
public IndentingBuilder()
30+
{
31+
_stringBuilder = new StringBuilder();
32+
}
33+
34+
/// <summary>
35+
/// Removes trailing whitespace from every line and replace all newlines with
36+
/// Environment.NewLine.
37+
/// </summary>
38+
private void Normalize()
39+
{
40+
_stringBuilder.Replace("\r\n", "\n");
41+
42+
// Remove trailing whitespace from every line
43+
int wsStart;
44+
for (int i = 0; i < _stringBuilder.Length; i++)
45+
{
46+
if (_stringBuilder[i] is '\n')
47+
{
48+
wsStart = i - 1;
49+
while (wsStart >= 0 && (_stringBuilder[wsStart] is ' ' or '\t'))
50+
{
51+
wsStart--;
52+
}
53+
wsStart++; // Move back to first whitespace
54+
if (wsStart < i)
55+
{
56+
int len = i - wsStart;
57+
_stringBuilder.Remove(wsStart, len);
58+
i -= len;
59+
}
60+
}
61+
}
62+
63+
_stringBuilder.Replace("\n", Environment.NewLine);
64+
}
65+
66+
public override string ToString()
67+
{
68+
Normalize();
69+
return _stringBuilder.ToString();
70+
}
71+
72+
public void Append(
73+
[InterpolatedStringHandlerArgument("")]
74+
SourceBuilderStringHandler s)
75+
{
76+
// No work needed, the handler has already added the text to the string builder
77+
}
78+
79+
public void Append(string s)
80+
{
81+
_stringBuilder.Append(_currentIndentWhitespace);
82+
Append(_stringBuilder, _currentIndentWhitespace, s);
83+
}
84+
85+
public void Append(IndentingBuilder srcBuilder)
86+
{
87+
Append(srcBuilder.ToString());
88+
}
89+
90+
private static void Append(
91+
StringBuilder builder,
92+
string currentIndentWhitespace,
93+
string str)
94+
{
95+
int start = 0;
96+
int nl;
97+
while (start < str.Length)
98+
{
99+
nl = str.IndexOf('\n', start);
100+
if (nl == -1)
101+
{
102+
nl = str.Length;
103+
}
104+
// Skip blank lines
105+
while (nl < str.Length && (str[nl] == '\n' || str[nl] == '\r'))
106+
{
107+
nl++;
108+
}
109+
if (start > 0)
110+
{
111+
builder.Append(currentIndentWhitespace);
112+
}
113+
builder.Append(str, start, nl - start);
114+
start = nl;
115+
}
116+
}
117+
118+
public void AppendLine(
119+
[InterpolatedStringHandlerArgument("")]
120+
SourceBuilderStringHandler s)
121+
{
122+
Append(s);
123+
_stringBuilder.AppendLine();
124+
}
125+
126+
public void AppendLine(string s)
127+
{
128+
Append(s);
129+
_stringBuilder.AppendLine();
130+
}
131+
132+
public int CompareTo(IndentingBuilder? other)
133+
{
134+
if (other is null) return 1;
135+
136+
var lenCmp = _stringBuilder.Length.CompareTo(other._stringBuilder.Length);
137+
if (lenCmp != 0)
138+
{
139+
return lenCmp;
140+
}
141+
for (int i = 0; i < _stringBuilder.Length; i++)
142+
{
143+
var cCmp = _stringBuilder[i].CompareTo(other._stringBuilder[i]);
144+
if (cCmp != 0)
145+
{
146+
return cCmp;
147+
}
148+
}
149+
return 0;
150+
}
151+
152+
public void Indent()
153+
{
154+
_currentIndentWhitespace += " ";
155+
}
156+
157+
public void Dedent()
158+
{
159+
_currentIndentWhitespace = _currentIndentWhitespace[..^4];
160+
}
161+
162+
public bool Equals(IndentingBuilder? other)
163+
{
164+
return _stringBuilder.Equals(other?._stringBuilder);
165+
}
166+
167+
public void AppendLine(IndentingBuilder deserialize)
168+
{
169+
Append(deserialize);
170+
_stringBuilder.AppendLine();
171+
}
172+
173+
[InterpolatedStringHandler]
174+
public ref struct SourceBuilderStringHandler
175+
{
176+
internal readonly StringBuilder _stringBuilder;
177+
private readonly string _originalIndentWhitespace;
178+
private string _currentIndentWhitespace;
179+
private bool _isFirst = true;
180+
181+
public SourceBuilderStringHandler(int literalLength, int formattedCount)
182+
{
183+
_stringBuilder = new StringBuilder(literalLength);
184+
_originalIndentWhitespace = "";
185+
_currentIndentWhitespace = "";
186+
}
187+
188+
public SourceBuilderStringHandler(
189+
int literalLength,
190+
int formattedCount,
191+
IndentingBuilder sourceBuilder)
192+
{
193+
_stringBuilder = sourceBuilder._stringBuilder;
194+
_originalIndentWhitespace = sourceBuilder._currentIndentWhitespace;
195+
_currentIndentWhitespace = sourceBuilder._currentIndentWhitespace;
196+
}
197+
198+
public void AppendLiteral(string s)
199+
{
200+
if (_isFirst)
201+
{
202+
_stringBuilder.Append(_currentIndentWhitespace);
203+
_isFirst = false;
204+
}
205+
Append(_stringBuilder, _currentIndentWhitespace, s);
206+
207+
int last = s.LastIndexOf('\n');
208+
if (last == -1)
209+
{
210+
return;
211+
}
212+
213+
var remaining = s.AsSpan(last + 1);
214+
foreach (var c in remaining)
215+
{
216+
if (c is not (' ' or '\t'))
217+
{
218+
return;
219+
}
220+
}
221+
222+
_currentIndentWhitespace += remaining.ToString();
223+
}
224+
225+
public void AppendFormatted<T>(T value)
226+
{
227+
if (_isFirst)
228+
{
229+
_stringBuilder.Append(_currentIndentWhitespace);
230+
_isFirst = false;
231+
}
232+
var str = value?.ToString();
233+
if (str is null)
234+
{
235+
_stringBuilder.Append(str);
236+
return;
237+
}
238+
239+
Append(_stringBuilder, _currentIndentWhitespace, str);
240+
_currentIndentWhitespace = _originalIndentWhitespace;
241+
}
242+
}
243+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<LangVersion Condition="'$(TargetFramework)' == 'netstandard2.0'">12.0</LangVersion>
8+
</PropertyGroup>
9+
10+
<PropertyGroup>
11+
<PackageId>StaticCS.IndentingBuilder</PackageId>
12+
<Version>0.1.0</Version>
13+
<IsPackable>true</IsPackable>
14+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
15+
<RepositoryUrl>https://github.com/agocke/static-cs</RepositoryUrl>
16+
<Description>A string builder with automatic indentation support for multi-line text generation.</Description>
17+
<Authors>agocke</Authors>
18+
<PackageTags>string-builder;indentation;code-generation;text-generation</PackageTags>
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="PolySharp" Version="1.15.0">
23+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24+
<PrivateAssets>all</PrivateAssets>
25+
</PackageReference>
26+
<PackageReference Include="System.Memory" Version="4.6.3" />
27+
</ItemGroup>
28+
29+
</Project>

0 commit comments

Comments
 (0)