From e0e3644ca98bb8cb95ee39049b1fefdc1f25dc81 Mon Sep 17 00:00:00 2001 From: "Chris S." Date: Sun, 15 Jun 2025 00:36:25 +0100 Subject: [PATCH] Add nested template test and streamline input stream --- src/TextTemplate/Template.cs | 73 +++++++++++++++++++ src/TextTemplate/TemplateEngine.cs | 17 ++++- .../TextTemplate.Tests/TemplateClassTests.cs | 41 +++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/TextTemplate/Template.cs create mode 100644 tests/TextTemplate.Tests/TemplateClassTests.cs diff --git a/src/TextTemplate/Template.cs b/src/TextTemplate/Template.cs new file mode 100644 index 0000000..a4ed0fd --- /dev/null +++ b/src/TextTemplate/Template.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Antlr4.Runtime; + +namespace TextTemplate; + +/// +/// Represents a text template similar to Go's template.Template. +/// +public class Template +{ + private string _templateString = string.Empty; + + /// + /// The template name. + /// + public string Name { get; } + + private Template(string name) + { + Name = name; + } + + /// + /// Creates a new Template with the given name. + /// + public static Template New(string name) => new(name); + + /// + /// Parses the supplied template string and returns the Template instance. + /// + public Template Parse(string templateString) + { + if (templateString == null) throw new ArgumentNullException(nameof(templateString)); + // Validate using the TemplateEngine so parsing rules remain consistent. + TemplateEngine.Validate(templateString); + _templateString = templateString; + return this; + } + + /// + /// Reads the given files, combines their contents, and parses the result. + /// + public Template ParseFiles(params string[] filenames) + { + if (filenames == null) throw new ArgumentNullException(nameof(filenames)); + var sb = new StringBuilder(); + foreach (var file in filenames) + { + sb.Append(File.ReadAllText(file)); + } + return Parse(sb.ToString()); + } + + /// + /// Executes the template using the provided dictionary. + /// + public string Execute(IDictionary data) + { + return TemplateEngine.Process(_templateString, data); + } + + /// + /// Executes the template using the public properties of . + /// + public string Execute(T model) + { + return TemplateEngine.Process(_templateString, model); + } + +} diff --git a/src/TextTemplate/TemplateEngine.cs b/src/TextTemplate/TemplateEngine.cs index 11d2cf0..d72dd81 100644 --- a/src/TextTemplate/TemplateEngine.cs +++ b/src/TextTemplate/TemplateEngine.cs @@ -24,7 +24,7 @@ public static string Process(string templateString, IDictionary { templateString = PreprocessWhitespace(templateString); templateString = PreprocessComments(templateString); - AntlrInputStream inputStream = new(templateString); + var inputStream = new AntlrInputStream(templateString); var lexer = new GoTextTemplateLexer(inputStream); var tokens = new CommonTokenStream(lexer); var parser = new GoTextTemplateParser(tokens); @@ -35,6 +35,21 @@ public static string Process(string templateString, IDictionary return visitor.Visit(tree); } + /// + /// Parses to ensure it contains valid syntax. + /// + public static void Validate(string templateString) + { + if (templateString == null) throw new ArgumentNullException(nameof(templateString)); + templateString = PreprocessWhitespace(templateString); + templateString = PreprocessComments(templateString); + var inputStream = new AntlrInputStream(templateString); + var lexer = new GoTextTemplateLexer(inputStream); + var tokens = new CommonTokenStream(lexer); + var parser = new GoTextTemplateParser(tokens); + parser.template(); + } + /// /// Processes using the public properties /// and fields of as template variables. diff --git a/tests/TextTemplate.Tests/TemplateClassTests.cs b/tests/TextTemplate.Tests/TemplateClassTests.cs new file mode 100644 index 0000000..9cdeef4 --- /dev/null +++ b/tests/TextTemplate.Tests/TemplateClassTests.cs @@ -0,0 +1,41 @@ +using Shouldly; +using TextTemplate; +using Xunit; +using System.IO; + +namespace TextTemplate.Tests; + +public class TemplateClassTests +{ + [Fact] + public void NewParseExecutePattern_Works() + { + var tmpl = Template.New("t").Parse("Hello {{ .Name }}! {{ range .Items }}{{ . }} {{ end }}"); + var result = tmpl.Execute(new { Name = "Bob", Items = new[] { "a", "b" } }); + result.ShouldBe("Hello Bob! a b "); + } + + [Fact] + public void ParseFiles_ReadsAndParsesAllFiles() + { + var f1 = Path.GetTempFileName(); + var f2 = Path.GetTempFileName(); + var f3 = Path.GetTempFileName(); + try + { + File.WriteAllText(f1, "{{define \"header\"}}Hello {{.Name}}{{end}}"); + File.WriteAllText(f2, "{{define \"exclaim\"}}!{{end}}"); + File.WriteAllText(f3, "{{template \"header\" .}}{{template \"exclaim\"}}"); + + var tmpl = Template.New("t").ParseFiles(f1, f2, f3); + var result = tmpl.Execute(new { Name = "Ann" }); + result.ShouldBe("Hello Ann!"); + } + finally + { + File.Delete(f1); + File.Delete(f2); + File.Delete(f3); + } + } +}