diff options
Diffstat (limited to 'store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs')
-rw-r--r-- | store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs b/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs new file mode 100644 index 0000000..1fe91b1 --- /dev/null +++ b/store/works/Crupest.SecretTool/Crupest.SecretTool/Template.cs @@ -0,0 +1,231 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Crupest.SecretTool; + +public class Template +{ + private enum ParseState + { + Text, + Dollar, + LeftBracket, + VariableName, + VariableNameFinish, + } + + private interface ITemplateNode + { + string Render(Dictionary<string, string> values); + } + + private class TextNode(string text) : ITemplateNode + { + + public string Text { get; } = text; + + public string Render(Dictionary<string, string> values) + { + return Text; + } + } + + private class VariableNode(string variableName) : ITemplateNode + { + public string VariableName { get; } = variableName; + + public string Render(Dictionary<string, string> values) + { + return values.GetValueOrDefault(VariableName) ?? ""; + } + } + + public Template(string templateString) + { + TemplateString = templateString; + Nodes = Parse(templateString); + VariableNames = Nodes.OfType<VariableNode>().Select(node => node.VariableName).ToList(); + } + + private static List<ITemplateNode> Parse(string templateString) + { + int lineNumber = 1; + int columnNumber = 0; + List<ITemplateNode> nodes = []; + ParseState state = ParseState.Text; + StringBuilder stringBuilder = new(); + + string GetPosition() => $"line {lineNumber} column{columnNumber}"; + + [DoesNotReturn] + void ReportInvalidState(string message) + { + throw new Exception($"Invalid state at {GetPosition()}: {message}"); + } + + [DoesNotReturn] + void ReportInvalidCharacter(char c) + { + throw new FormatException($"Unexpected '{c}' at {GetPosition()}."); + } + + void FinishText() + { + if (state != ParseState.Text) + { + ReportInvalidState($"Can't call FinishText here."); + } + + if (stringBuilder.Length > 0) + { + nodes.Add(new TextNode(stringBuilder.ToString())); + stringBuilder.Clear(); + } + } + + foreach (var c in templateString) + { + if (c == '\n') + { + lineNumber++; + columnNumber = 0; + } + + columnNumber++; + + switch (c) + { + case '$': + if (state == ParseState.Text) + { + FinishText(); + state = ParseState.Dollar; + } + else if (state == ParseState.Dollar) + { + if (stringBuilder.Length > 0) + { + throw new Exception($"Invalid state at {GetPosition()}: when we meet the second '$', text builder should be empty."); + } + stringBuilder.Append(c); + state = ParseState.Text; + } + else + { + throw new FormatException($"Unexpected '$' at {GetPosition()}."); + } + break; + case '{': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.Dollar) + { + state = ParseState.LeftBracket; + } + else + { + throw new Exception($"Unexpected '{{' at {GetPosition()}."); + } + break; + case '}': + if (state == ParseState.Text) + { + stringBuilder.Append(c); + state = ParseState.Text; + } + else if (state == ParseState.VariableName || state == ParseState.VariableNameFinish) + { + nodes.Add(new VariableNode(stringBuilder.ToString())); + stringBuilder.Clear(); + state = ParseState.Text; + } + else + { + ReportInvalidCharacter(c); + } + break; + default: + if (state == ParseState.Dollar) + { + ReportInvalidCharacter(c); + } + + if (char.IsWhiteSpace(c)) + { + if (state == ParseState.LeftBracket || state == ParseState.VariableNameFinish) + { + continue; + } + else if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.VariableName) + { + state = ParseState.VariableNameFinish; + } + else + { + ReportInvalidCharacter(c); + } + } + else + { + if (state == ParseState.Text) + { + stringBuilder.Append(c); + } + else if (state == ParseState.LeftBracket || state == ParseState.VariableName) + { + stringBuilder.Append(c); + state = ParseState.VariableName; + } + else + { + ReportInvalidCharacter(c); + } + } + break; + } + } + + if (state == ParseState.Text) + { + FinishText(); + } + else + { + throw new FormatException("Unexpected end of template string."); + } + + return nodes; + } + + public string TemplateString { get; } + private List<ITemplateNode> Nodes { get; set; } + public List<string> VariableNames { get; } + + public string Generate(Dictionary<string, string> values, bool allowMissingVariable = false) + { + StringBuilder stringBuilder = new(); + foreach (var node in Nodes) + { + if (node is TextNode textNode) + { + stringBuilder.Append(textNode.Text); + } + else if (node is VariableNode variableNode) + { + var hasValue = values.TryGetValue(variableNode.VariableName, out var value); + if (!hasValue && !allowMissingVariable) + { + throw new Exception($"Variable '{variableNode.VariableName}' is not set."); + } + stringBuilder.Append(hasValue ? value : string.Empty); + } + } + return stringBuilder.ToString(); + } +} |