diff --git a/src/TextTemplate/TemplateEngine.cs b/src/TextTemplate/TemplateEngine.cs index c86d492..f51e38b 100644 --- a/src/TextTemplate/TemplateEngine.cs +++ b/src/TextTemplate/TemplateEngine.cs @@ -22,6 +22,7 @@ public static class TemplateEngine /// public static string Process(string templateString, IDictionary data) { + templateString = PreprocessAssignments(templateString); templateString = PreprocessWhitespace(templateString); templateString = PreprocessComments(templateString); AntlrInputStream inputStream = new(templateString); @@ -80,6 +81,14 @@ private static string PreprocessComments(string template) return Regex.Replace(template, "\\{\\{-?\\s*/\\*.*?\\*/\\s*-?\\}\\}", string.Empty, RegexOptions.Singleline); } + private static string PreprocessAssignments(string template) + { + return Regex.Replace(template, + "\\{\\{\\s*\\$([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?::=|=)\\s*(.*?)\\s*\\}\\}", + m => $"{{{{ assign \"{m.Groups[1].Value}\" {m.Groups[2].Value} }}}}", + RegexOptions.Singleline); + } + private static IDictionary ToDictionary(object model) { var dict = new Dictionary(); @@ -522,18 +531,35 @@ public override string VisitBlockBlock(GoTextTemplateParser.BlockBlockContext co { if (text == "$" || text == "$.") return _rootCurrent; - text = text.StartsWith("$.") ? text.Substring(2) : text.Substring(1); - if (string.IsNullOrEmpty(text)) - return _rootCurrent; - var segmentsRoot = ParseSegments(text); - object? currentRoot = _rootData; - foreach (var seg in segmentsRoot) + + if (text.StartsWith("$.")) { - if (currentRoot == null) - return null; - currentRoot = ResolveSegment(currentRoot, seg); + text = text.Substring(2); + if (string.IsNullOrEmpty(text)) + return _rootCurrent; + var segs = ParseSegments(text); + object? cur = _rootData; + foreach (var seg in segs) + { + if (cur == null) + return null; + cur = ResolveSegment(cur, seg); + } + return cur; + } + else + { + text = text.Substring(1); + var segs = ParseSegments(text); + object? cur = _data; + foreach (var seg in segs) + { + if (cur == null) + return null; + cur = ResolveSegment(cur, seg); + } + return cur; } - return currentRoot; } var segments = ParseSegments(text); @@ -653,6 +679,17 @@ public override string VisitBlockBlock(GoTextTemplateParser.BlockBlockContext co private object? ApplyPipelineFunction(string name, params object?[] args) { + if (name == "assign") + { + if (args.Length >= 2) + { + string varName = args[0]?.ToString() ?? string.Empty; + _data[varName] = args[1]!; + return string.Empty; + } + return string.Empty; + } + if (PipelineFuncs.TryGetValue(name, out var fn)) return fn(args); return args.Length > 0 ? args[0] : null; diff --git a/tests/TextTemplate.Tests/TemplateEngineTests.cs b/tests/TextTemplate.Tests/TemplateEngineTests.cs index 3fd8f22..9999696 100644 --- a/tests/TextTemplate.Tests/TemplateEngineTests.cs +++ b/tests/TextTemplate.Tests/TemplateEngineTests.cs @@ -670,4 +670,31 @@ public void Range_DotAndRootAccess() }); result.ShouldBe("- a root;- b root;"); } + + [Fact] + public void VariableDeclaration() + { + const string tmpl = "{{ $g := \"Hi\" }}{{ $g }}"; + var result = TemplateEngine.Process(tmpl, new Dictionary()); + result.ShouldBe("Hi"); + } + + [Fact] + public void VariableReassignment() + { + const string tmpl = "{{ $g := \"Hi\" }}{{ $g = \"Bye\" }}{{ $g }}"; + var result = TemplateEngine.Process(tmpl, new Dictionary()); + result.ShouldBe("Bye"); + } + + [Fact] + public void RangeLoopWithIndexAndValue() + { + const string tmpl = "{{ range $i, $v := .Items }}{{ $i }}: {{ $v }};{{ end }}"; + var result = TemplateEngine.Process(tmpl, new Dictionary + { + ["Items"] = new[] { "a", "b" } + }); + result.ShouldBe("0: a;1: b;"); + } }