diff --git a/.editorconfig b/.editorconfig
index 1e10e86..2ed42e2 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,18 +1,25 @@
[*.cs]
csharp_style_prefer_primary_constructors = false
dotnet_style_prefer_collection_expression = false
+csharp_indent_braces = false
+csharp_new_line_before_open_brace = all
# ReSharper properties
resharper_align_multiline_binary_expressions_chain = false
resharper_braces_for_foreach = required
resharper_braces_for_ifelse = required
resharper_braces_for_while = required
+resharper_braces_redundant = true
resharper_csharp_keep_blank_lines_in_code = 1
resharper_csharp_keep_blank_lines_in_declarations = 1
resharper_csharp_wrap_before_binary_opsign = true
resharper_csharp_wrap_parameters_style = chop_if_long
+resharper_max_attribute_length_for_same_line = 60
resharper_method_or_operator_body = expression_body
resharper_place_single_method_argument_lambda_on_same_line = false
resharper_wrap_after_declaration_lpar = true
resharper_wrap_after_invocation_lpar = true
resharper_wrap_chained_binary_expressions = chop_if_long
+
+# Microsoft .NET properties
+csharp_prefer_braces = true:none
diff --git a/CodeMe.ScaffoldCS.sln.DotSettings b/CodeMe.ScaffoldCS.sln.DotSettings
index ed3bda4..8d9d380 100644
--- a/CodeMe.ScaffoldCS.sln.DotSettings
+++ b/CodeMe.ScaffoldCS.sln.DotSettings
@@ -5,9 +5,15 @@
True
<?xml version="1.0" encoding="utf-16"?><Profile name="Cleanup on Save"><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" ArrangeAccessors="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeEmptyString="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" ArrangeBraces="True" /><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><XMLReformatCode>True</XMLReformatCode><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSharpReformatComments>True</CSharpReformatComments><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><RemoveCodeRedundancies>True</RemoveCodeRedundancies></Profile>
Required
+ DO_NOT_CHANGE
+ False
ZeroIndent
False
<Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></Policy>
+ True
+ True
+ True
+ True
True
\ No newline at end of file
diff --git a/CodeMe.ScaffoldCS.slnx b/CodeMe.ScaffoldCS.slnx
index 76332cd..ea37f3d 100644
--- a/CodeMe.ScaffoldCS.slnx
+++ b/CodeMe.ScaffoldCS.slnx
@@ -7,9 +7,11 @@
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6d54c12..b889258 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,8 @@
+
-
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 1c2b6d1..ad8bada 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,32 @@
# CodeMe.ScaffoldCS
Scaffolding framework inspired by CodegenCS
+
+# Template issues
+
+Have a good scenarios:
+* Last line behavior (last line is empty)
+ * May be fixed if we keep endline in the current line and trim endline on close
+ (last line behavior)
+ * Will also fix inconsistency with delayed flush on MacOS ('\r' separator)
+* Consider to split Template and advanced rendering logic. The goal is to have api that allows to add third-party rendering customisation
+* Add api for column-level padding. Func to PaddedTemplatePart?
+ * Need both options.
+* Split template / text writer options
+
+Waits for grooming:
+* Value handling customizations. Interceptors?
+ * We need a really good scenario for it. With naive implementation interceptors will slow down each write operation so lets keep it on hold.
+* Review behaviors for `// {SummaryWithNewLineAtTheEnd} something` render scenario. Something like NextLineIndentation or EnforceIndentation() maybe?
+* Shall we append indentation on whitespace-only lines?
+ * Do we need "append indentation on empty lines" option?
+* Add support for alignment arg for multiline values?
+ * Need a good scenario for this. We have auto-indentation for this
+* Multiline value nahdling.
+ * Add option for Template to disable multiline indentation. Add .RenderLiteral() / .RenderMultiline()
+ * Or just add .RenderLiteral() and no option
+
+Delayed:
+* Docs are missing
+* No XML comments
+
+# Metamodel issues
\ No newline at end of file
diff --git a/docs/TemplateRender.md b/docs/TemplateRender.md
new file mode 100644
index 0000000..294acd1
--- /dev/null
+++ b/docs/TemplateRender.md
@@ -0,0 +1,9 @@
+# Summary
+
+Render logic is based on c# [interpolated dtring handler](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/interpolated-string-handler) feature.
+
+Also, we use ambient template context (based on [ScopedAsyncLocal<T>](https://github.com/ig-sinicyn/CodeMe/tree/master/docs/Basics/AsyncLocal)) to ease passing of the current writer to the nested parts.
+
+We do support conditional evaluation by IIF/IF-ELSE-ENDIF macro.
+
+We do support conditional line trim by RL (RemoveLine macro)
diff --git a/src/CodeMe.ScaffoldCS.Templates/CodeMe.ScaffoldCS.Templates.csproj b/src/CodeMe.ScaffoldCS.Templates/CodeMe.ScaffoldCS.Templates.csproj
new file mode 100644
index 0000000..50c7988
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/CodeMe.ScaffoldCS.Templates.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+ net10.0
+ Template render engine inspired by CodegenCS
+ Scaffolld;Template;Codegen;Code Generation
+
+
+
+
+
+
+
diff --git a/src/CodeMe.ScaffoldCS.Templates/Infrastructure/CharSpanExtensions.cs b/src/CodeMe.ScaffoldCS.Templates/Infrastructure/CharSpanExtensions.cs
new file mode 100644
index 0000000..c29934b
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Infrastructure/CharSpanExtensions.cs
@@ -0,0 +1,20 @@
+namespace CodeMe.ScaffoldCS.Templates.Infrastructure;
+
+internal static class CharSpanExtensions
+{
+ public static int NextIndexOfAny(
+ this ReadOnlySpan span,
+ int startIndex,
+ char value0,
+ char value1)
+ {
+ var index = span[startIndex..].IndexOfAny(value0, value1);
+ return index < 0 ? index : startIndex + index;
+ }
+
+ public static bool IsMultiline(this ReadOnlySpan span)
+ {
+ var index = span.IndexOfAny('\r', '\n');
+ return index >= 0;
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/AmbientTemplate.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/AmbientTemplate.cs
new file mode 100644
index 0000000..6bb04d6
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/AmbientTemplate.cs
@@ -0,0 +1,16 @@
+using CodeMe.Basics.Threading;
+
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public static class AmbientTemplate
+{
+ private static readonly ScopedAsyncLocal _templateStack = new();
+
+ internal static IDisposable BeginTemplateScope(Template context) => _templateStack.BeginScope(context);
+
+ public static Template Current => _templateStack.Current
+ ?? throw new InvalidOperationException(
+ $"There is no ambient template. Call {nameof(Template)}.{nameof(Template.BeginAmbientScope)} to enable ambient templates");
+
+ public static Template? CurrentOrDefault => _templateStack.Current;
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/ConditionBranch.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/ConditionBranch.cs
new file mode 100644
index 0000000..470d22b
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/ConditionBranch.cs
@@ -0,0 +1,9 @@
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public enum ConditionBranch
+{
+ None,
+ IfBranch,
+ ElseIfBranch,
+ ElseBranch
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/IControlTemplateSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/IControlTemplateSymbol.cs
new file mode 100644
index 0000000..1588ef9
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/IControlTemplateSymbol.cs
@@ -0,0 +1,5 @@
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public interface IControlTemplateSymbol : ITemplateSymbol
+{
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/IScopedTemplateValue.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/IScopedTemplateValue.cs
new file mode 100644
index 0000000..0ac64e1
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/IScopedTemplateValue.cs
@@ -0,0 +1,6 @@
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public interface ITemplateScope
+{
+ void DisposeIfSame(Template caller);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplatePart.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplatePart.cs
new file mode 100644
index 0000000..0f2589b
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplatePart.cs
@@ -0,0 +1,24 @@
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public interface ITemplatePart : ITemplateScope
+{
+ void AppendLiteral(string? value);
+
+ void AppendFormatted(T? value);
+
+ void AppendFormatted(T? value, int alignment);
+
+ void AppendFormatted(T? value, int alignment, string? format);
+
+ void AppendFormatted(T? value, string? format);
+
+ void AppendFormatted(string? value);
+
+ void AppendFormatted(string? value, int alignment);
+
+ void AppendFormatted(Action callback);
+
+ void AppendFormatted(Func callback);
+
+ void AppendFormatted(IEnumerable values);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplateSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplateSymbol.cs
new file mode 100644
index 0000000..026623a
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Internals/ITemplateSymbol.cs
@@ -0,0 +1,6 @@
+namespace CodeMe.ScaffoldCS.Templates.Internals;
+
+public interface ITemplateSymbol
+{
+ void Render(Template template);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineHandlingMode.cs b/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineHandlingMode.cs
new file mode 100644
index 0000000..781ab46
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineHandlingMode.cs
@@ -0,0 +1,12 @@
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+public enum CurrentLineHandlingMode
+{
+ Normal,
+
+ IgnoreIfEmpty,
+
+ IgnoreIfWhiteSpace,
+
+ Ignore
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineState.cs b/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineState.cs
new file mode 100644
index 0000000..ae4ca0e
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/CurrentLineState.cs
@@ -0,0 +1,14 @@
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+internal enum CurrentLineState
+{
+ Empty,
+
+ ValueAppended,
+
+ CrAppended,
+
+ LfAppended,
+
+ Closed
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/IndentationFormat.cs b/src/CodeMe.ScaffoldCS.Templates/Output/IndentationFormat.cs
new file mode 100644
index 0000000..ea9808b
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/IndentationFormat.cs
@@ -0,0 +1,14 @@
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+public enum IndentationFormat
+{
+ CurrentLine,
+
+ CurrentLineWhitespace,
+
+ CurrentIndentation,
+
+ IncreasedIndentation,
+
+ Empty
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/NewLineFormat.cs b/src/CodeMe.ScaffoldCS.Templates/Output/NewLineFormat.cs
new file mode 100644
index 0000000..2353b81
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/NewLineFormat.cs
@@ -0,0 +1,12 @@
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+public enum NewLineFormat
+{
+ Auto,
+
+ CrLf,
+
+ Lf,
+
+ Cr
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriter.cs b/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriter.cs
new file mode 100644
index 0000000..d9e0af8
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriter.cs
@@ -0,0 +1,438 @@
+using System.Text;
+using CodeMe.ScaffoldCS.Templates.Infrastructure;
+
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+internal sealed class TemplateTextWriter : IDisposable
+{
+ private readonly TextWriter _output;
+ private readonly bool _leaveOutputOpen;
+ private readonly StringBuilder _currentLine;
+ private StringBuilder.AppendInterpolatedStringHandler _currentLineFormatter;
+ private CurrentLineState _currentLineState;
+ private CurrentLineHandlingMode _currentLineHandlingMode;
+
+ public TemplateTextWriter(TextWriter output, TemplateTextWriterOptions? options = null, bool leaveOpen = false)
+ {
+ _output = output;
+ _leaveOutputOpen = leaveOpen;
+ _currentLine = new StringBuilder();
+ Options = options ?? TemplateTextWriterOptions.Default;
+ }
+
+ public TemplateTextWriterOptions Options
+ {
+ get;
+ set
+ {
+ var cultureChanged = !ReferenceEquals(value.Culture, field?.Culture);
+ field = value;
+ if (cultureChanged)
+ {
+ InitFormatter();
+ }
+ }
+ }
+
+ private void InitFormatter() =>
+ _currentLineFormatter = new StringBuilder.AppendInterpolatedStringHandler(0, 0, _currentLine, Options.Culture);
+
+ public ReadOnlyMemory GetCurrentLineIndentation(IndentationFormat format)
+ {
+ if (format == IndentationFormat.Empty)
+ {
+ return Array.Empty();
+ }
+
+ if (format == IndentationFormat.CurrentIndentation || _currentLineState != CurrentLineState.ValueAppended)
+ {
+ return Options.Indentation;
+ }
+
+ if (format == IndentationFormat.IncreasedIndentation || _currentLineState != CurrentLineState.ValueAppended)
+ {
+ return string.Concat(Options.Indentation.Span, Options.SingleIndentation.Span).AsMemory();
+ }
+
+ var result = new char[_currentLine.Length];
+ _currentLine.CopyTo(0, result, _currentLine.Length);
+
+ if (format == IndentationFormat.CurrentLineWhitespace)
+ {
+ for (var i = 0; i < result.Length; i++)
+ {
+ if (!char.IsWhiteSpace(result[i]))
+ {
+ result[i] = ' ';
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public TemplateTextWriter SetCurrentLineHandling(CurrentLineHandlingMode lineHandling)
+ {
+ _currentLineHandlingMode = lineHandling;
+ return this;
+ }
+
+ public TemplateTextWriter ClearLine()
+ {
+ ClearLineCore();
+ return this;
+ }
+
+ public TemplateTextWriter Append(T? value)
+ {
+ if (value is null)
+ {
+ return AppendCore([]);
+ }
+
+ if (value is string @string)
+ {
+ return AppendCore(@string);
+ }
+
+ if (value is ReadOnlyMemory memory)
+ {
+ return AppendCore(memory.Span);
+ }
+
+ BeforeAppend(CurrentLineState.ValueAppended);
+ _currentLineFormatter.AppendFormatted(value);
+ AfterAppend(CurrentLineState.ValueAppended);
+
+ return this;
+ }
+
+ public TemplateTextWriter Append(T? value, int alignment, string? format)
+ {
+ if (value is null)
+ {
+ return AppendCore([], alignment);
+ }
+
+ if (value is string @string)
+ {
+ return AppendCore(@string, alignment);
+ }
+
+ if (value is ReadOnlyMemory memory)
+ {
+ return AppendCore(memory.Span, alignment);
+ }
+
+ BeforeAppend(CurrentLineState.ValueAppended);
+ _currentLineFormatter.AppendFormatted(value, alignment, format);
+ AfterAppend(CurrentLineState.ValueAppended);
+
+ return this;
+ }
+
+ public TemplateTextWriter AppendLine() =>
+ AppendCore(GetNewLine());
+
+ public TemplateTextWriter Append(string? value) =>
+ AppendCore(value);
+
+ public TemplateTextWriter Append(string? value, int alignment) =>
+ AppendCore(value, alignment);
+
+ public TemplateTextWriter Append(ReadOnlyMemory value) =>
+ AppendCore(value.Span);
+
+ public TemplateTextWriter Append(ReadOnlyMemory value, int alignment) =>
+ AppendCore(value.Span, alignment);
+
+ public TemplateTextWriter Append(ReadOnlySpan value) =>
+ AppendCore(value);
+
+ public TemplateTextWriter Append(ReadOnlySpan value, int alignment) =>
+ AppendCore(value, alignment);
+
+ private TemplateTextWriter AppendCore(ReadOnlySpan value, int alignment)
+ {
+ // Align to right. Newlines are ignored.
+ if (alignment > value.Length)
+ {
+ BeforeAppend(CurrentLineState.ValueAppended);
+ _currentLine.Append(' ', alignment - value.Length);
+ AfterAppend(CurrentLineState.ValueAppended);
+ }
+
+ // Append value with newline handling.
+ AppendCore(value);
+
+ // Align to left. Newlines are ignored
+ if (-alignment > value.Length)
+ {
+ BeforeAppend(CurrentLineState.ValueAppended);
+ _currentLine.Append(' ', -alignment - value.Length);
+ AfterAppend(CurrentLineState.ValueAppended);
+ }
+
+ return this;
+ }
+
+ private TemplateTextWriter AppendCore(ReadOnlySpan value)
+ {
+ if (value.IsEmpty)
+ {
+ return this;
+ }
+
+ var index = 0;
+ while (index < value.Length)
+ {
+ switch (value[index])
+ {
+ case '\r':
+ BeforeAppend(CurrentLineState.CrAppended);
+ _currentLine.Append(value[index]);
+ index++;
+ AfterAppend(CurrentLineState.CrAppended);
+ break;
+
+ case '\n':
+ BeforeAppend(CurrentLineState.LfAppended);
+ _currentLine.Append(value[index]);
+ index++;
+ AfterAppend(CurrentLineState.LfAppended);
+ break;
+
+ default:
+ BeforeAppend(CurrentLineState.ValueAppended);
+ var lineEndIndex = value.NextIndexOfAny(index, '\r', '\n');
+ var nextIndex = lineEndIndex < 0 ? value.Length : lineEndIndex;
+ _currentLine.Append(value[index..nextIndex]);
+ index = nextIndex;
+ AfterAppend(CurrentLineState.ValueAppended);
+ break;
+ }
+ }
+
+ return this;
+ }
+
+ public void Close()
+ {
+ if (_currentLineState == CurrentLineState.Closed)
+ {
+ return;
+ }
+
+ BeforeClose();
+ if (!_leaveOutputOpen)
+ {
+ _output.Dispose();
+ }
+
+ AfterClose();
+ }
+
+ public void Dispose() => Close();
+
+ private void BeforeAppend(CurrentLineState nextLineState)
+ {
+ AssertNotClosed();
+
+ if (_currentLineState is CurrentLineState.CrAppended && nextLineState is not CurrentLineState.LfAppended)
+ {
+ NormalizeCurrentLineEnd();
+ FlushCoreAndResetState();
+ }
+
+ var indentation = Options.Indentation;
+ if (_currentLineState is CurrentLineState.Empty && !indentation.IsEmpty)
+ {
+ if (nextLineState == CurrentLineState.ValueAppended)
+ {
+ _currentLine.Append(indentation);
+ _currentLineState = CurrentLineState.ValueAppended;
+ }
+
+ if (nextLineState is CurrentLineState.CrAppended or CurrentLineState.LfAppended
+ && Options.AppendIndentationOnEmptyLines)
+ {
+ _currentLine.Append(indentation);
+ _currentLineState = CurrentLineState.ValueAppended;
+ }
+ }
+
+ if (_currentLineState is CurrentLineState.ValueAppended
+ && nextLineState is CurrentLineState.CrAppended or CurrentLineState.LfAppended)
+ {
+ TrimCurrentLineEnd();
+ }
+ }
+
+ private void AfterAppend(CurrentLineState nextLineState)
+ {
+ if (nextLineState is CurrentLineState.LfAppended)
+ {
+ NormalizeCurrentLineEnd();
+ FlushCoreAndResetState();
+ }
+ else
+ {
+ _currentLineState = nextLineState;
+ }
+ }
+
+ private void BeforeClose()
+ {
+ AssertNotClosed();
+
+ if (_currentLineState is CurrentLineState.CrAppended or CurrentLineState.LfAppended)
+ {
+ NormalizeCurrentLineEnd();
+ }
+
+ if (_currentLineState is CurrentLineState.ValueAppended)
+ {
+ TrimCurrentLineEnd();
+ }
+
+ FlushCoreAndResetState();
+ }
+
+ private void AfterClose() => _currentLineState = CurrentLineState.Closed;
+
+ private void ClearLineCore()
+ {
+ AssertNotClosed();
+
+ if (_currentLineState is CurrentLineState.ValueAppended)
+ {
+ _currentLine.Clear();
+ _currentLineState = CurrentLineState.Empty;
+ }
+ }
+
+ private void AssertNotClosed()
+ {
+ if (_currentLineState == CurrentLineState.Closed)
+ {
+ throw new ObjectDisposedException("Cannot append text to closed template");
+ }
+ }
+
+ private string GetNewLine() =>
+ Options.NewLineFormat switch
+ {
+ NewLineFormat.Auto => Environment.NewLine,
+ NewLineFormat.CrLf => "\r\n",
+ NewLineFormat.Lf => "\n",
+ NewLineFormat.Cr => "\r",
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ private void TrimCurrentLineEnd()
+ {
+ if (!Options.TrimLineEnd)
+ {
+ return;
+ }
+
+ var newLength = 0;
+ for (var i = _currentLine.Length - 1; i >= 0; i--)
+ {
+ if (!char.IsWhiteSpace(_currentLine[i]))
+ {
+ newLength = i + 1;
+ break;
+ }
+ }
+
+ if (_currentLine.Length != newLength)
+ {
+ _currentLine.Length = newLength;
+ }
+ }
+
+ private void NormalizeCurrentLineEnd()
+ {
+ if (!Options.NormalizeNewLines)
+ {
+ return;
+ }
+
+ switch (_currentLine)
+ {
+ case [.., '\r', '\n']:
+ _currentLine.Length -= 2;
+ _currentLine.Append(GetNewLine());
+ break;
+ case [.., '\r']:
+ _currentLine.Length -= 1;
+ _currentLine.Append(GetNewLine());
+ break;
+ case [.., '\n']:
+ _currentLine.Length -= 1;
+ _currentLine.Append(GetNewLine());
+ break;
+ }
+ }
+
+ private void FlushCoreAndResetState()
+ {
+ AssertNotClosed();
+
+ if (ShouldWriteToOutput())
+ {
+ _output.Write(_currentLine);
+ }
+
+ _currentLine.Clear();
+ _currentLineState = CurrentLineState.Empty;
+ _currentLineHandlingMode = CurrentLineHandlingMode.Normal;
+ }
+
+ private bool ShouldWriteToOutput() =>
+ _currentLineHandlingMode switch
+ {
+ CurrentLineHandlingMode.Normal => _currentLine.Length > 0,
+ CurrentLineHandlingMode.IgnoreIfEmpty => !CurrentLineIsEmpty(),
+ CurrentLineHandlingMode.IgnoreIfWhiteSpace => !CurrentLineIsWhiteSpace(),
+ CurrentLineHandlingMode.Ignore => false,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+
+ private bool CurrentLineIsEmpty() =>
+ _currentLine switch
+ {
+ [] => true,
+ ['\r', '\n'] => true,
+ ['\r'] => true,
+ ['\n'] => true,
+ _ => false
+ };
+
+ private bool CurrentLineIsWhiteSpace()
+ {
+ for (var i = _currentLine.Length - 1; i >= 0; i--)
+ {
+ if (!char.IsWhiteSpace(_currentLine[i]))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ internal TestAccessor GetTestAccessor() => new(this);
+
+ internal readonly struct TestAccessor(TemplateTextWriter owner)
+ {
+ public TextWriter Output => owner._output;
+
+ public string CurrentLine => owner._currentLine.ToString();
+
+ public CurrentLineState CurrentLineState => owner._currentLineState;
+
+ public CurrentLineHandlingMode CurrentLineHandling => owner._currentLineHandlingMode;
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriterOptions.cs b/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriterOptions.cs
new file mode 100644
index 0000000..abfbde3
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Output/TemplateTextWriterOptions.cs
@@ -0,0 +1,22 @@
+using System.Globalization;
+
+namespace CodeMe.ScaffoldCS.Templates.Output;
+
+public sealed record TemplateTextWriterOptions(
+ CultureInfo Culture,
+ ReadOnlyMemory Indentation,
+ ReadOnlyMemory SingleIndentation,
+ NewLineFormat NewLineFormat,
+ bool AppendIndentationOnEmptyLines,
+ bool TrimLineEnd,
+ bool NormalizeNewLines)
+{
+ public static readonly TemplateTextWriterOptions Default = new(
+ Culture: CultureInfo.InvariantCulture,
+ Indentation: Array.Empty().AsMemory(),
+ SingleIndentation: "\t".AsMemory(),
+ NewLineFormat: NewLineFormat.Auto,
+ AppendIndentationOnEmptyLines: true,
+ TrimLineEnd: true,
+ NormalizeNewLines: true);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/ClearLineSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Render/ClearLineSymbol.cs
new file mode 100644
index 0000000..4ee204e
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/ClearLineSymbol.cs
@@ -0,0 +1,8 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct ClearLineSymbol : ITemplateSymbol
+{
+ public void Render(Template template) => template.ClearLine();
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderExtensions.cs b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderExtensions.cs
new file mode 100644
index 0000000..12dd8e2
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderExtensions.cs
@@ -0,0 +1,26 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public static class CollectionRenderExtensions
+{
+ public static ITemplateSymbol Render(this IEnumerable value, CollectionRenderOptions? options = null) =>
+ new CollectionSymbol(value, null, options ?? CollectionRenderOptions.Default);
+
+ public static ITemplateSymbol Render(
+ this IEnumerable value,
+ Func format,
+ CollectionRenderOptions? options = null) =>
+ new CollectionSymbol(value, format, options ?? CollectionRenderOptions.Default);
+
+ public static ITemplateSymbol RenderCommaSeparated(this IEnumerable value) =>
+ new CollectionSymbol(value, null, CollectionRenderOptions.CommaSeparated);
+
+ public static ITemplateSymbol RenderEmptyLineSeparated(this IEnumerable value) =>
+ new CollectionSymbol(value, null, CollectionRenderOptions.EmptyLineSeparated);
+
+ public static ITemplateSymbol RenderEmptyLineSeparated(
+ this IEnumerable value,
+ Func format) =>
+ new CollectionSymbol(value, format, CollectionRenderOptions.EmptyLineSeparated);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderOptions.cs b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderOptions.cs
new file mode 100644
index 0000000..c7fbef9
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionRenderOptions.cs
@@ -0,0 +1,21 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public record CollectionRenderOptions(
+ string? Separator = null,
+ ITemplateSymbol? EmptyValue = null,
+ int Alignment = 0,
+ string? Format = null)
+{
+ public static readonly CollectionRenderOptions CommaSeparated = new(Separator: ", ");
+
+ public static readonly CollectionRenderOptions Multiline = new(Separator: Environment.NewLine);
+
+ public static readonly CollectionRenderOptions EmptyLineSeparated =
+ new(
+ Separator: Environment.NewLine + Environment.NewLine,
+ EmptyValue: Symbols.RemoveWhitespaceLine);
+
+ public static readonly CollectionRenderOptions Default = Multiline;
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/CollectionSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionSymbol.cs
new file mode 100644
index 0000000..9d6a4c9
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/CollectionSymbol.cs
@@ -0,0 +1,45 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct CollectionSymbol(
+ IEnumerable source,
+ Func? callback,
+ CollectionRenderOptions options) : ITemplateSymbol
+{
+ public void Render(Template template)
+ {
+ var hasFormat = callback == null && (options.Alignment != 0 || options.Format != null);
+
+ var hasItem = false;
+ using var _ = template.BeginOptionsScope(IndentationFormat.CurrentLine);
+ foreach (var item in source)
+ {
+ if (hasItem && !string.IsNullOrEmpty(options.Separator))
+ {
+ template.WriteLiteral(options.Separator);
+ }
+
+ if (callback != null)
+ {
+ template.Write(callback(item));
+ }
+ else if (hasFormat)
+ {
+ template.Write(item, options.Alignment, options.Format);
+ }
+ else
+ {
+ template.Write(item);
+ }
+
+ hasItem = true;
+ }
+
+ if (!hasItem && options.EmptyValue != null)
+ {
+ template.Write(options.EmptyValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/ConditionBranchSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionBranchSymbol.cs
new file mode 100644
index 0000000..0a276b3
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionBranchSymbol.cs
@@ -0,0 +1,44 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct ConditionBranchSymbol(
+ ConditionBranch branch,
+ bool condition,
+ CurrentLineHandlingMode? lineHandling)
+ : IControlTemplateSymbol
+{
+ public void Render(Template template)
+ {
+ // We need to set line handling before and after branch switching
+ // as we may toggle output on or off
+ if (lineHandling != null)
+ {
+ template.SetLineHandling(lineHandling.Value);
+ }
+
+ switch (branch)
+ {
+ case ConditionBranch.None:
+ template.CompleteIfBranch();
+ break;
+ case ConditionBranch.IfBranch:
+ template.BeginIfBranch(condition);
+ break;
+ case ConditionBranch.ElseIfBranch:
+ template.BeginElseIfBranch(condition);
+ break;
+ case ConditionBranch.ElseBranch:
+ template.BeginElseBranch();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(branch), branch, null);
+ }
+
+ if (lineHandling != null)
+ {
+ template.SetLineHandling(lineHandling.Value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`1.cs b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`1.cs
new file mode 100644
index 0000000..bd403e7
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`1.cs
@@ -0,0 +1,17 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct ConditionSymbol(
+ bool condition,
+ TTrue trueValue)
+ : ITemplateSymbol
+{
+ public void Render(Template template)
+ {
+ if (condition)
+ {
+ template.Write(trueValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`2.cs b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`2.cs
new file mode 100644
index 0000000..c620e2a
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/ConditionSymbol`2.cs
@@ -0,0 +1,22 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct ConditionSymbol(
+ bool condition,
+ TTrue trueValue,
+ TFalse falseValue)
+ : ITemplateSymbol
+{
+ public void Render(Template template)
+ {
+ if (condition)
+ {
+ template.Write(trueValue);
+ }
+ else
+ {
+ template.Write(falseValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/Conditions.cs b/src/CodeMe.ScaffoldCS.Templates/Render/Conditions.cs
new file mode 100644
index 0000000..88361aa
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/Conditions.cs
@@ -0,0 +1,37 @@
+using System.Collections;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public static class Conditions
+{
+ public static bool HasValue(T? value) =>
+ value switch
+ {
+ null => false,
+ string s => !string.IsNullOrEmpty(s),
+ ICollection collection => collection.Count > 0,
+ IEnumerable enumerable => HasValue(enumerable),
+ _ => !EqualityComparer.Default.Equals(value, default!)
+ };
+
+ public static bool HasValue(ReadOnlySpan value) => value.Length > 0;
+
+ public static bool HasValue(ReadOnlyMemory value) => value.Length > 0;
+
+ public static bool HasValue(IReadOnlyCollection value) => value.Count > 0;
+
+ public static bool HasValue(IReadOnlyDictionary value) => value.Count > 0;
+
+ private static bool HasValue(IEnumerable enumerable)
+ {
+ var enumerator = enumerable.GetEnumerator();
+ try
+ {
+ return enumerator.MoveNext();
+ }
+ finally
+ {
+ (enumerator as IDisposable)?.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/IfFalseTemplatePart.cs b/src/CodeMe.ScaffoldCS.Templates/Render/IfFalseTemplatePart.cs
new file mode 100644
index 0000000..b420a57
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/IfFalseTemplatePart.cs
@@ -0,0 +1,68 @@
+using System.Runtime.CompilerServices;
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+[InterpolatedStringHandler]
+public readonly struct IfFalseTemplatePart : ITemplateScope
+{
+ private readonly Template _template;
+ private readonly Template.OptionsScope _scope;
+
+ public IfFalseTemplatePart(
+ int literalLength,
+ int formattedCount,
+ TCondition condition,
+ out bool isEnabled)
+ : this(literalLength, formattedCount, AmbientTemplate.Current, condition, out isEnabled)
+ {
+ }
+
+ public IfFalseTemplatePart(
+ int literalLength,
+ int formattedCount,
+ Template template,
+ TCondition condition,
+ out bool isEnabled)
+ {
+ isEnabled = !Conditions.HasValue(condition);
+ _template = template;
+ if (isEnabled)
+ {
+ _scope = template.BeginTemplatePartScope();
+ }
+ }
+
+ void ITemplateScope.DisposeIfSame(Template caller) =>
+ _scope.DisposeIfSame(caller);
+
+ public void AppendLiteral(string? value) =>
+ _template.WriteLiteral(value);
+
+ public void AppendFormatted(T? value) =>
+ _template.Write(value);
+
+ public void AppendFormatted(T? value, int alignment) =>
+ _template.Write(value, alignment);
+
+ public void AppendFormatted(T? value, int alignment, string? format) =>
+ _template.Write(value, alignment, format);
+
+ public void AppendFormatted(T? value, string? format) =>
+ _template.Write(value, 0, format);
+
+ public void AppendFormatted(string? value) =>
+ _template.Write(value);
+
+ public void AppendFormatted(string? value, int alignment) =>
+ _template.Write(value, alignment);
+
+ public void AppendFormatted(Action callback) =>
+ callback(_template);
+
+ public void AppendFormatted(Func callback) =>
+ _template.Write(callback());
+
+ public void AppendFormatted(IEnumerable values) =>
+ _template.Write(values);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/IfTrueTemplatePart.cs b/src/CodeMe.ScaffoldCS.Templates/Render/IfTrueTemplatePart.cs
new file mode 100644
index 0000000..13cbc92
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/IfTrueTemplatePart.cs
@@ -0,0 +1,68 @@
+using System.Runtime.CompilerServices;
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+[InterpolatedStringHandler]
+public readonly struct IfTrueTemplatePart : ITemplatePart
+{
+ private readonly Template _template;
+ private readonly Template.OptionsScope _scope;
+
+ public IfTrueTemplatePart(
+ int literalLength,
+ int formattedCount,
+ TCondition condition,
+ out bool isEnabled)
+ : this(literalLength, formattedCount, AmbientTemplate.Current, condition, out isEnabled)
+ {
+ }
+
+ public IfTrueTemplatePart(
+ int literalLength,
+ int formattedCount,
+ Template template,
+ TCondition condition,
+ out bool isEnabled)
+ {
+ isEnabled = Conditions.HasValue(condition);
+ _template = template;
+ if (isEnabled)
+ {
+ _scope = template.BeginTemplatePartScope();
+ }
+ }
+
+ void ITemplateScope.DisposeIfSame(Template caller) =>
+ _scope.DisposeIfSame(caller);
+
+ public void AppendLiteral(string? value) =>
+ _template.WriteLiteral(value);
+
+ public void AppendFormatted(T? value) =>
+ _template.Write(value);
+
+ public void AppendFormatted(T? value, int alignment) =>
+ _template.Write(value, alignment);
+
+ public void AppendFormatted(T? value, int alignment, string? format) =>
+ _template.Write(value, alignment, format);
+
+ public void AppendFormatted(T? value, string? format) =>
+ _template.Write(value, 0, format);
+
+ public void AppendFormatted(string? value) =>
+ _template.Write(value);
+
+ public void AppendFormatted(string? value, int alignment) =>
+ _template.Write(value, alignment);
+
+ public void AppendFormatted(Action callback) =>
+ callback(_template);
+
+ public void AppendFormatted(Func callback) =>
+ _template.Write(callback());
+
+ public void AppendFormatted(IEnumerable values) =>
+ _template.Write(values);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/LineHandlingSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Render/LineHandlingSymbol.cs
new file mode 100644
index 0000000..7a8a821
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/LineHandlingSymbol.cs
@@ -0,0 +1,9 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct LineHandlingSymbol(CurrentLineHandlingMode lineHandling) : ITemplateSymbol
+{
+ public void Render(Template template) => template.SetLineHandling(lineHandling);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/Symbols.cs b/src/CodeMe.ScaffoldCS.Templates/Render/Symbols.cs
new file mode 100644
index 0000000..4a47633
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/Symbols.cs
@@ -0,0 +1,62 @@
+using System.Runtime.CompilerServices;
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public static class Symbols
+{
+ private static readonly CurrentLineHandlingMode _macroLineHandling = CurrentLineHandlingMode.IgnoreIfWhiteSpace;
+
+ public static readonly ValueSymbol NewLine = new(Environment.NewLine);
+
+ public static readonly ClearLineSymbol ClearLine = new();
+
+ public static readonly LineHandlingSymbol RemoveLine = new(CurrentLineHandlingMode.Ignore);
+
+ public static readonly LineHandlingSymbol RemoveEmptyLine = new(CurrentLineHandlingMode.IgnoreIfEmpty);
+
+ public static readonly LineHandlingSymbol RemoveWhitespaceLine = new(CurrentLineHandlingMode.IgnoreIfWhiteSpace);
+
+ public static ConditionBranchSymbol If(T condition) => new(
+ ConditionBranch.IfBranch, Conditions.HasValue(condition), _macroLineHandling);
+
+ public static ConditionBranchSymbol ElseIf(T condition) => new(
+ ConditionBranch.ElseIfBranch, Conditions.HasValue(condition), _macroLineHandling);
+
+ public static readonly ConditionBranchSymbol Else = new(ConditionBranch.ElseBranch, false, _macroLineHandling);
+
+ public static readonly ConditionBranchSymbol EndIf = new(ConditionBranch.None, false, _macroLineHandling);
+
+ public static ConditionSymbol Iif(T condition, TTrue trueValue) =>
+ new(Conditions.HasValue(condition), trueValue);
+
+ public static ConditionSymbol> Iif(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue) =>
+ new(Conditions.HasValue(condition), trueValue);
+
+ public static ConditionSymbol Iif(
+ T condition,
+ TTrue trueValue,
+ TFalse falseValue) =>
+ new(Conditions.HasValue(condition), trueValue, falseValue);
+
+ public static ConditionSymbol, IfFalseTemplatePart> Iif(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfFalseTemplatePart falseValue) =>
+ new(Conditions.HasValue(condition), trueValue, falseValue);
+
+ public static ConditionSymbol, TFalse> Iif(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue,
+ TFalse falseValue) =>
+ new(Conditions.HasValue(condition), trueValue, falseValue);
+
+ public static ConditionSymbol> Iif(
+ T condition,
+ TTrue trueValue,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfFalseTemplatePart falseValue) =>
+ new(Conditions.HasValue(condition), trueValue, falseValue);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/SymbolsCS.cs b/src/CodeMe.ScaffoldCS.Templates/Render/SymbolsCS.cs
new file mode 100644
index 0000000..cc3e5ca
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/SymbolsCS.cs
@@ -0,0 +1,55 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+[SuppressMessage("ReSharper", "InconsistentNaming")]
+public static class SymbolsCS
+{
+ public static readonly ValueSymbol NL = Symbols.NewLine;
+
+ public static readonly ClearLineSymbol CL = Symbols.ClearLine;
+
+ public static readonly LineHandlingSymbol RL = Symbols.RemoveLine;
+
+ public static readonly LineHandlingSymbol REL = Symbols.RemoveEmptyLine;
+
+ public static readonly LineHandlingSymbol RWL = Symbols.RemoveWhitespaceLine;
+
+ public static ConditionBranchSymbol IF(T value) => Symbols.If(value);
+
+ public static ConditionBranchSymbol ELSEIF(T value) => Symbols.ElseIf(value);
+
+ public static readonly ConditionBranchSymbol ELSE = Symbols.Else;
+
+ public static readonly ConditionBranchSymbol ENDIF = Symbols.EndIf;
+
+ public static ConditionSymbol IIF(T condition, TTrue trueValue) =>
+ Symbols.Iif(condition, trueValue);
+
+ public static ConditionSymbol> IIF(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue) =>
+ Symbols.Iif(condition, trueValue);
+
+ public static ConditionSymbol IIF(T condition, TTrue trueValue, TFalse falseValue) =>
+ Symbols.Iif(condition, trueValue, falseValue);
+
+ public static ConditionSymbol, IfFalseTemplatePart> IIF(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfFalseTemplatePart falseValue) =>
+ Symbols.Iif(condition, trueValue, falseValue);
+
+ public static ConditionSymbol, TFalse> IIF(
+ T condition,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfTrueTemplatePart trueValue,
+ TFalse falseValue) =>
+ Symbols.Iif(condition, trueValue, falseValue);
+
+ public static ConditionSymbol> IIF(
+ T condition,
+ TTrue trueValue,
+ [InterpolatedStringHandlerArgument(nameof(condition))] IfFalseTemplatePart falseValue) =>
+ Symbols.Iif(condition, trueValue, falseValue);
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Render/ValueSymbol.cs b/src/CodeMe.ScaffoldCS.Templates/Render/ValueSymbol.cs
new file mode 100644
index 0000000..010d9d7
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Render/ValueSymbol.cs
@@ -0,0 +1,19 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+
+namespace CodeMe.ScaffoldCS.Templates.Render;
+
+public readonly struct ValueSymbol(T value, int alignment = 0, string? format = null) : ITemplateSymbol
+{
+ public void Render(Template template)
+ {
+ var hasFormat = alignment != 0 || format != null;
+ if (hasFormat)
+ {
+ template.Write(value, alignment, format);
+ }
+ else
+ {
+ template.Write(value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Template.Context.cs b/src/CodeMe.ScaffoldCS.Templates/Template.Context.cs
new file mode 100644
index 0000000..bcbe892
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Template.Context.cs
@@ -0,0 +1,115 @@
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates;
+
+public partial class Template
+{
+ private readonly Stack _optionsStack = new();
+ private readonly Stack _branchesStack = new();
+
+ private BranchState CurrentBranch => _branchesStack.TryPeek(out var x) ? x : BranchState.NoBranch;
+
+ public OptionsScope BeginOptionsScope(IndentationFormat indentationFormat) =>
+ BeginOptionsScope(NewOptions(indentationFormat));
+
+ public OptionsScope BeginOptionsScope(TemplateTextWriterOptions options)
+ {
+ var oldOptions = Options;
+ _optionsStack.Push(oldOptions);
+ Options = options;
+ return new OptionsScope(this, oldOptions);
+ }
+
+ internal OptionsScope BeginTemplatePartScope() =>
+ BeginOptionsScope(IndentationFormat.CurrentLine);
+
+ private TemplateTextWriterOptions NewOptions(IndentationFormat indentationFormat) => Options with
+ {
+ Indentation = _writer.GetCurrentLineIndentation(indentationFormat)
+ };
+
+ private void CompleteScope(TemplateTextWriterOptions options)
+ {
+ if (_optionsStack.Count == 0 || !ReferenceEquals(_optionsStack.Peek(), options))
+ {
+ throw new InvalidOperationException("Cannot complete scope as current options do not match expected ones.");
+ }
+
+ Options = options;
+ _optionsStack.Pop();
+ }
+
+ public void BeginIfBranch(bool condition)
+ {
+ var parent = CurrentBranch;
+
+ var shouldWrite = condition && parent.ConditionMet;
+ _branchesStack.Push(new BranchState(ConditionBranch.IfBranch, shouldWrite, shouldWrite));
+ ShouldWrite = shouldWrite;
+ }
+
+ public void BeginElseIfBranch(bool condition)
+ {
+ var previous = CurrentBranch;
+ if (previous.Branch is not (ConditionBranch.IfBranch or ConditionBranch.ElseIfBranch))
+ {
+ throw new InvalidOperationException($"Cannot begin ElseIf branch after {previous.Branch}");
+ }
+
+ var shouldWrite = condition && !previous.AnyBranchMet;
+ var anyBranchMet = condition || previous.AnyBranchMet;
+ _branchesStack.Pop();
+ _branchesStack.Push(new BranchState(ConditionBranch.ElseIfBranch, shouldWrite, anyBranchMet));
+ ShouldWrite = shouldWrite;
+ }
+
+ public void BeginElseBranch()
+ {
+ var previous = CurrentBranch;
+ if (previous.Branch is not (ConditionBranch.IfBranch or ConditionBranch.ElseIfBranch))
+ {
+ throw new InvalidOperationException($"Cannot begin Else branch after {previous.Branch}");
+ }
+
+ var shouldWrite = !previous.AnyBranchMet;
+ var anyBranchMet = true;
+ _branchesStack.Pop();
+ _branchesStack.Push(new BranchState(ConditionBranch.ElseIfBranch, shouldWrite, anyBranchMet));
+ ShouldWrite = shouldWrite;
+ }
+
+ public void CompleteIfBranch()
+ {
+ var previous = CurrentBranch;
+ if (previous.Branch == ConditionBranch.None)
+ {
+ throw new InvalidOperationException("Cannot complete if/else branch as there is no active branch");
+ }
+
+ _branchesStack.Pop();
+ var current = CurrentBranch;
+ ShouldWrite = current.ConditionMet;
+ }
+
+ public readonly struct OptionsScope(Template owner, TemplateTextWriterOptions oldOptions)
+ : ITemplateScope, IDisposable
+ {
+ public void DisposeIfSame(Template caller)
+ {
+ if (ReferenceEquals(caller, owner))
+ {
+ Dispose();
+ }
+ }
+
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
+ public void Dispose() => owner?.CompleteScope(oldOptions);
+ }
+
+ internal readonly record struct BranchState(ConditionBranch Branch, bool ConditionMet, bool AnyBranchMet)
+ {
+ public static readonly BranchState NoBranch = new(
+ ConditionBranch.None, ConditionMet: true, AnyBranchMet: true);
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Template.TestAccessor.cs b/src/CodeMe.ScaffoldCS.Templates/Template.TestAccessor.cs
new file mode 100644
index 0000000..44f1300
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Template.TestAccessor.cs
@@ -0,0 +1,21 @@
+using CodeMe.ScaffoldCS.Templates.Output;
+
+namespace CodeMe.ScaffoldCS.Templates;
+
+public partial class Template
+{
+ internal TestAccessor GetTestAccessor() => new(this);
+
+ internal readonly struct TestAccessor(Template owner)
+ {
+ private readonly TemplateTextWriter.TestAccessor _writerAccessor = owner._writer.GetTestAccessor();
+
+ public TextWriter Output => _writerAccessor.Output;
+
+ public IReadOnlyCollection OptionsStack => owner._optionsStack;
+
+ public BranchState CurrentBranch => owner.CurrentBranch;
+
+ public IReadOnlyCollection BranchesStack => owner._branchesStack;
+ }
+}
\ No newline at end of file
diff --git a/src/CodeMe.ScaffoldCS.Templates/Template.Write.cs b/src/CodeMe.ScaffoldCS.Templates/Template.Write.cs
new file mode 100644
index 0000000..4c67cfa
--- /dev/null
+++ b/src/CodeMe.ScaffoldCS.Templates/Template.Write.cs
@@ -0,0 +1,319 @@
+using System.Collections;
+using System.Runtime.CompilerServices;
+using CodeMe.ScaffoldCS.Templates.Infrastructure;
+using CodeMe.ScaffoldCS.Templates.Internals;
+using CodeMe.ScaffoldCS.Templates.Output;
+using CodeMe.ScaffoldCS.Templates.Render;
+
+namespace CodeMe.ScaffoldCS.Templates;
+
+public partial class Template
+{
+ public Template SetLineHandling(CurrentLineHandlingMode lineHandling)
+ {
+ if (ShouldWrite)
+ {
+ _writer.SetCurrentLineHandling(lineHandling);
+ }
+
+ return this;
+ }
+
+ public Template ClearLine()
+ {
+ if (ShouldWrite)
+ {
+ _writer.ClearLine();
+ }
+
+ return this;
+ }
+
+ public Template WriteLine()
+ {
+ if (ShouldWrite)
+ {
+ _writer.AppendLine();
+ }
+
+ return this;
+ }
+
+ public Template WriteLiteral(string? value)
+ {
+ if (ShouldWrite)
+ {
+ _writer.Append(value);
+ }
+
+ return this;
+ }
+
+ public Template Write(string? value)
+ {
+ Write(value.AsSpan());
+ return this;
+ }
+
+ public Template Write(string? value, int alignment)
+ {
+ Write(value.AsSpan(), alignment);
+ return this;
+ }
+
+ public Template WriteLine(string? value)
+ {
+ Write(value);
+ WriteLine();
+
+ return this;
+ }
+
+ public Template Write(ReadOnlyMemory value)
+ {
+ Write(value.Span);
+
+ return this;
+ }
+
+ public Template Write(ReadOnlyMemory value, int alignment)
+ {
+ Write(value.Span, alignment);
+
+ return this;
+ }
+
+ public Template WriteLine(ReadOnlyMemory value)
+ {
+ WriteLine(value.Span);
+
+ return this;
+ }
+
+ public Template Write(ReadOnlySpan value)
+ {
+ if (ShouldWrite)
+ {
+ if (value.IsMultiline())
+ {
+ using var _ = BeginOptionsScope(IndentationFormat.CurrentLine);
+ _writer.Append(value);
+ }
+ else
+ {
+ _writer.Append(value);
+ }
+ }
+
+ return this;
+ }
+
+ public Template Write(ReadOnlySpan value, int alignment)
+ {
+ if (ShouldWrite)
+ {
+ _writer.Append(value, alignment);
+ }
+
+ return this;
+ }
+
+ public Template WriteLine(ReadOnlySpan value)
+ {
+ Write(value);
+ WriteLine();
+
+ return this;
+ }
+
+ public Template Write(IEnumerable value)
+ {
+ if (ShouldWrite)
+ {
+ value.Cast