Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/TextTemplate/Template.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Antlr4.Runtime;

namespace TextTemplate;

/// <summary>
/// Represents a text template similar to Go's template.Template.
/// </summary>
public class Template
{
private string _templateString = string.Empty;

/// <summary>
/// The template name.
/// </summary>
public string Name { get; }

private Template(string name)
{
Name = name;
}

/// <summary>
/// Creates a new Template with the given name.
/// </summary>
public static Template New(string name) => new(name);

/// <summary>
/// Parses the supplied template string and returns the Template instance.
/// </summary>
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;
}

/// <summary>
/// Reads the given files, combines their contents, and parses the result.
/// </summary>
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());
}

/// <summary>
/// Executes the template using the provided dictionary.
/// </summary>
public string Execute(IDictionary<string, object> data)
{
return TemplateEngine.Process(_templateString, data);
}

/// <summary>
/// Executes the template using the public properties of <typeparamref name="T"/>.
/// </summary>
public string Execute<T>(T model)
{
return TemplateEngine.Process(_templateString, model);
}

}
17 changes: 16 additions & 1 deletion src/TextTemplate/TemplateEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
{
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);
Expand All @@ -35,6 +35,21 @@
return visitor.Visit(tree);
}

/// <summary>
/// Parses <paramref name="templateString"/> to ensure it contains valid syntax.
/// </summary>
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();
}

/// <summary>
/// Processes <paramref name="templateString"/> using the public properties
/// and fields of <paramref name="model"/> as template variables.
Expand Down Expand Up @@ -142,7 +157,7 @@
if (args.Length == 0) return string.Empty;
string fmt = args[0]?.ToString() ?? string.Empty;
var rest = args.Skip(1).ToArray();
return SprintfFormatter.Format(fmt, rest);

Check warning on line 160 in src/TextTemplate/TemplateEngine.cs

View workflow job for this annotation

GitHub Actions / build

Argument of type 'object?[]' cannot be used for parameter 'args' of type 'object[]' in 'string SprintfFormatter.Format(string format, params object[] args)' due to differences in the nullability of reference types.
},
["html"] = args => System.Net.WebUtility.HtmlEncode(string.Concat(args.Select(a => a?.ToString()))),
["js"] = args => System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(string.Concat(args.Select(a => a?.ToString()))),
Expand Down Expand Up @@ -174,7 +189,7 @@
}
if (current is IDictionary dict)
{
current = dict[key];

Check warning on line 192 in src/TextTemplate/TemplateEngine.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'key' in 'object? IDictionary.this[object key]'.
continue;
}
current = null;
Expand Down
41 changes: 41 additions & 0 deletions tests/TextTemplate.Tests/TemplateClassTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading