Building a Template Engine - Part 1
At this point, I'd like to mention a couple of articles that I found very educational:
- http://www.codeproject.com/KB/recipes/Nested_RegEx_explained.aspx
- 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 103 | using 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; } } } |
Simple variables are replaced using the following code:
| 1 | var r = new Regex(@"\$(?<name>([\w/_\.]*)\b)"); |
As a small enhancement to basic substitution of variables, we can also cater for basic for...next loops. The following line:
| 1 | var fl = new Regex(@"#foreach\(\$(?<iterator>[^)]*)\sin\s\$(?<enumerable>[^)]*)\) (?<code>[\$\w\s\<\>\/.]*) #end"); |
'#foreach $<iterator> in $<enumerable> <code> #end'.
Note that ' is matched using the
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 20 | static 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(); } |
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.

Comments: