Building a Template Engine - Part 1

Being able to master templates is a useful skill for a programmer, not only are templates a major part of many languages (XSLT, ASP.NET to name but two), but templates are also useful for code autogeneration and quick manipulation of code and data. Although there are templating tools available, I thought I'd investigate how simple it was to create a basic templating engine. One side goal of this is that I would like to eventually create a templating engine that I could use in my MVC framework instead of NVelocity (just for the challenge!). To begin with, I decided to see how far I could get using the regular expression library (RegEx) in .NET:

At this point, I'd like to mention a couple of articles that I found very educational:

  1. http://www.codeproject.com/KB/recipes/Nested_RegEx_explained.aspx
  2. http://www.codeproject.com/KB/recipes/RegEx_Balanced_Grouping.aspx

These articles explain how the advanced features of the .NET implementation of RegEx (including the presence of a stack) enable nested constructions to be matched.

Anyway, back to my simple template engine, here is the a very simple example:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.CodeDom.Compiler; using System.Reflection; using System.Text.RegularExpressions; using System.Collections; namespace FishySplash.Template { public class Templater { public string Transform(string template, Dictionary<string, object> variables) { // for loops var fl = new Regex(@"#foreach\(\$(?<iterator>[^)]*)\sin\s\$(?<enumerable>[^)]*)\) (?<code>[\$\w\s\<\>\/.]*) #end"); template = fl.Replace(template, m => { var iterator = m.Groups["iterator"].Value; var enumerable = m.Groups["enumerable"].Value; var code = m.Groups["code"].Value; // get enumerable object IEnumerable enumer = GetDictionaryElement(variables, enumerable) as IEnumerable; if (enumer != null) { string loopedText = ""; foreach (var iter in enumer) { Dictionary<string, object> iterationVariables = new Dictionary<string, object>(); iterationVariables = GetProperties(iter, iterationVariables, iterator, true); loopedText += Transform(code, iterationVariables); } return loopedText; } else return iterator; // leave text unchanged }); // simple expansion of variables var r = new Regex(@"\$(?<name>([\w/_\.]*)\b)"); template = SimpleReplace(template, r, variables); return template; } public string Transform(string template, object model) { var variables = GetProperties(model, new Dictionary<string, object>(), string.Empty, false); return Transform(template, variables); } private Dictionary<string, object> GetProperties(object model, Dictionary<string, object> properties, string prefix, bool includeRoot) { Type modelType = model.GetType(); if (includeRoot) properties.Add(prefix, model); foreach (var pi in model.GetType().GetProperties()) { if (!IsIndexedProperty(pi)) { object nextval = pi.GetValue(model, null); string nextprefix = prefix + (string.IsNullOrEmpty(prefix) ? string.Empty : ".") + pi.Name; properties = GetProperties(nextval, properties, nextprefix, true); } } return properties; } private bool IsIndexedProperty(PropertyInfo pi) { return (pi.GetIndexParameters().Count() > 0); } // possibly make this extension method private object GetDictionaryElement(Dictionary<string, object> dictionary, string key) { object ret = null; if (dictionary.TryGetValue(key, out ret)) return ret; else return null; } private string SimpleReplace(string template, Regex pattern, Dictionary<string, object> variables) { var result = pattern.Replace(template, m => { var key = m.Groups["name"].Value; object val; if (variables.TryGetValue(key, out val)) return val.ToString(); else return key; }); return result; } } }
This class has 2 public overrides for the Transform() method. In each case, the first parameter is a string representing the template to use. this template will contain 'placeholders' marked with a $ character followed by the variable name. In each example, the second parameter is used to pass in the variables to merge with the template. One overload uses a dictionary, the other uses an anonymous type.

Simple variables are replaced using the following code:

1var r = new Regex(@"\$(?<name>([\w/_\.]*)\b)");
This matches all occurances of ?. The simple replace method does the actual replacement using groups.

As a small enhancement to basic substitution of variables, we can also cater for basic for...next loops. The following line:

1var fl = new Regex(@"#foreach\(\$(?<iterator>[^)]*)\sin\s\$(?<enumerable>[^)]*)\) (?<code>[\$\w\s\<\>\/.]*) #end");
searches for matches to the following pattern:

'#foreach $<iterator> in $<enumerable> <code> #end'.

Note that '' must be an enumerable type. The enumerable object is iterated, and for each iteration, the template in is matched using the as the variable.

A simple test of this template engine can be done as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20static void Main(string[] args) { Templater t = new Templater(); Dictionary<string, object> Model = new Dictionary<string,object>(); Customer c = new Customer(); c.Age = 25; c.Name="Fred Smith"; c.Roles = new List<string>(); c.Roles.Add("Admin"); c.Roles.Add("Operator"); c.Roles.Add("Superuser"); Model.Add("Customer",c); string loop = "$Customer.Name is $Customer.Age. $Customer.Name's roles are: #foreach($role in $Customer.Roles) $role #end."; Console.Write(t.Transform(loop, new { Customer = c})); //Console.Write(t.Transform(mycode, Model)); Console.ReadKey(); }
The following output should be displayed:

Fred Smith is 25. Fred Smith's roles are: AdminOperatorSuperuser.
In upcoming articles, I'll try and add more features to make an even more useful, but still lightweight template engine.
David Barone, 5 August, 2009, 9:05 pm
Last Modified: 25 February, 2010, 8:00 pm

Comments:

Comments are closed for this article.