How to define template directives (from an API perspective)?
- by Ralph
Preface
I'm writing a template language (don't bother trying to talk me out of it), and in it, there are two kinds of user-extensible nodes. TemplateTags and TemplateDirectives.
A TemplateTag closely relates to an HTML tag -- it might look something like
div(class="green") { "content" }
And it'll be rendered as
<div class="green">content</div>
i.e., it takes a bunch of attributes, plus some content, and spits out some HTML.
TemplateDirectives are a little more complicated. They can be things like for loops, ifs, includes, and other such things. They look a lot like a TemplateTag, but they need to be processed differently. For example,
@for($i in $items) {
div(class="green") { $i }
}
Would loop over $items and output the content with the variable $i substituted in each time.
So.... I'm trying to decide on a way to define these directives now.
Template Tags
The TemplateTags are pretty easy to write. They look something like this:
[TemplateTag]
static string div(string content = null, object attrs = null)
{
return HtmlTag("div", content, attrs);
}
Where content gets the stuff between the curly braces (pre-rendered if there are variables in it and such), and attrs is either a Dictionary<string,object> of attributes, or an anonymous type used like a dictionary. It just returns the HTML which gets plunked into its place. Simple! You can write tags in basically 1 line.
Template Directives
The way I've defined them now looks like this:
[TemplateDirective]
static string @for(string @params, string content)
{
var tokens = Regex.Split(@params, @"\sin\s").Select(s => s.Trim()).ToArray();
string itemName = tokens[0].Substring(1);
string enumName = tokens[1].Substring(1);
var enumerable = data[enumName] as IEnumerable;
var sb = new StringBuilder();
var template = new Template(content);
foreach (var item in enumerable)
{
var templateVars = new Dictionary<string, object>(data) { { itemName, item } };
sb.Append(template.Render(templateVars));
}
return sb.ToString();
}
(Working example). Basically, the stuff between the ( and ) is not split into arguments automatically (like the template tags do), and the content isn't pre-rendered either.
The reason it isn't pre-rendered is because you might want to add or remove some template variables or something first. In this case, we add the $i variable to the template variables,
var templateVars = new Dictionary<string, object>(data) { { itemName, item } };
And then render the content manually,
sb.Append(template.Render(templateVars));
Question
I'm wondering if this is the best approach to defining custom Template Directives. I want to make it as easy as possible. What if the user doesn't know how to render templates, or doesn't know that he's supposed to?
Maybe I should pass in a Template instance pre-filled with the content instead? Or maybe only let him tamper w/ the template variables, and then automatically render the content at the end?
OTOH, for things like "if" if the condition fails, then the template wouldn't need to be rendered at all. So there's a lot of flexibility I need to allow in here.
Thoughts?