A simple templating engine

I wanted to talk about templating, since templating is a common thing you run into. Often times you want to cleanly do a string replace on a bunch of text, and sometimes even need minimal language processing to do what you want. For example, Java has a templating engine called Velocity, but lots of languages have libraries that do this kind of work. I thought it’d be fun to create a small templating engine from scratch with F# as an after work exercise.

The goal is to give the templating processor a set of lookup bags that can be resolved by variables. For example, if I use a variable $devshorts.isgreat that should correspond to a bag that is keyed first off of devshorts which returns a new bag, and then a new bag that has a key isgreat which should return a value.

Getting the AST

First, lets parse the language and get an abstract syntax tree. Anything that is prefixed with dollar sign is a language construct, anything not is a literal. As with most parsing tasks, I jump straight to fparsec.

I’ve exposed only one language construct (a for loop), and anything else is just a basic string replace bag (which will already be deconstructed into its individual components, i.e. $foo.bar will be ["foo";"bar"]).

Contexts

The next thing we need is a way to store a context, and to resolve a requested path from the context. Since I want to be able to add key value pairs to the context but have the values be different (sometimes they should be a string, other times they should be other context bags), we need to be able to handle that.

For example, lets say I make a context called “anton”. In this context I want to have key “isGreat” that resolves to “kropp”. That would end up being a leaf node in this context path. But how do I represent a path like “anton.shmanton.isGreat”. The key “shmanton” should resolve to a new context under the current context of “anton”. Also, in order to leverage for loops, we need some keys to resolve to multiple values. So now we have 3 types of results: a string, a string list, or another context. Given that, lets create a context class that can handle creating these contexts, as well as resolving a context path.

One thing that is tricky here: ctxs.[h].resolve t doesn’t call the same resolve function on the Context class. It actually calls the resolve function on the ContextType. This way each type can resolve itself. If you call resolve on a string, it’ll return itself (as a list). If you resolve on a list, it’ll return the list. But, if you call resolve on a context, it’ll proxy that request back to the Context class.

You may also be wondering what “runTimeAdd” and “runtimeRemove” are. Those will make sense when we actually create the language interpreter. It may be a little overkill to call this a “language” but it kind of is!

Applying the context to the AST

Now we need to interpret the syntax tree and apply the context bag to any context related tokens. If anybody read my previous posts about my language I wrote, this should all sound pretty similar (cause it is!)

What we have here is an eval function that acts as the main interpreter dispatch loop. It’s asked to evaluate the current token its given based on its current context.

If we have a string literal, we just return it (as a list, since I am creating a list of evaluated results).

If there is a bag (like $anton.isgreat) then try and resolve the bag path from the context.

If there is a for loop we want to evaluate the result of the for predicate and bind its value to the alias. Then for each element we want to evaluate the contents of the for loop. This is where we need to create a runtime storage of the alias, so we can do later lookups in the context. You can see that each for loop adds its alias to the context and then removes it from the context afterwards. This would mimic a regular language where inner loops can access outer declared variables, but not vice versa.

> Runner.run root templateText;;
val it : string =
"The current song is come as you are!
Oh lets just loop again for fun. First value: come as you are, second: come as you are
Oh lets just loop again for fun. First value: come as you are, second: smells like teen spirit
The current song is smells like teen spirit!
Oh lets just loop again for fun. First value: smells like teen spirit, second: come as you are
Oh lets just loop again for fun. First value: smells like teen spirit, second: smells like teen spirit
"