Teeny Tiny Templates

On my current project, I need to "fill in the blanks" in some text file templates.  It’s nothing we haven’t all done before using a series of string.Replace functions, and they work just fine, but they’re not shiny enough.  Also, when you think about it, each individual Replace operation iterates over the whole document again looking for the next tag to replace.  I decided to make something using regular expressions instead.  Something that would make just ONE pass over the document.  Here’s the result.

/// <summary>Simple, lightweight template parser.</summary>
/// <remarks>Replaces ASP-style tags in the form <%tag%> or <%=tag%> in strings.</remarks>
public class TemplateParser
{
    private const string tagPattern = @"<%=*\s*(?<1>.*?)\s*%>";
    public Dictionary<string, object> TagValues { get; private set; }

    public TemplateParser()
    {
        TagValues = new Dictionary<string, object>();
    }

    public string ParseString(string template)
    {
        return Regex.Replace(template, tagPattern, match =>
        {
            string tag = match.Groups[1].Value;
            return (TagValues.ContainsKey(tag) 
                ? TagValues[tag].ToString() 
                : string.Empty);
        });
    }

    public string ParseFile(string filename)
    {
        return ParseString(File.ReadAllText(filename));
    }
}
Yeah, that’s the whole thing… really.  To use it, you just new up a TemplateParser, add items to the TagValues dictionary, and then call ParseString, handing in the template.  Here’s a simple unit test that shows it in action.
[TestClass]
public class TemplateParserTests
{
    private TemplateParser Target { get; set; }

    [TestInitialize]
    public void TestInitialize()
    {
        Target = new TemplateParser();
        Target.TagValues["A"] = 1;
        Target.TagValues["B"] = "2";
        Target.TagValues["C"] = 3;
    }

    [TestMethod]
    public void ParseString_replaces_tags()
    {
        string template = "A<%A%>B<%=B%>C<% C %>";
        string expected = "A1B2C3";
        string actual = Target.ParseString(template);
        Assert.AreEqual(expected,actual);
    }
}

Now on to the discussion.  There’s really only one interesting part to this whole thing, and that’s the call to Regex.Replace.  You can see I’m passing it a lambda expression here.  This is the MatchEvaluator.  Its whole job is to take each match and do something with it.  It could trim spaces, make it all uppercase, anything really.  In my case, I’m going to pull the tag out of the match, and look it up in the dictionary.  If I find a value I’ll replace the whole match with it, otherwise I’ll replace it with an empty string.

I suppose the regular expression (@"<%=*\s*(?<1>.*?)\s*%>") itself could use a little explaining.  I’ve often described regular expressions as a "write-only" medium.  You can look up the parts you need in a reference, string them together, and make a working RegEx, but woe unto those who try to read it after it’s been created.  So let’s break it down into chunks.

@ – This just says "Yes, I know the following string is ugly, deal with it."
(Not really, but I like saying that)

<% – This is our opening asp-style tag

=* – There may or may not be an equals sign here.  Either way, I don’t care.

\s* – There may or may not be some whitespace here.  Same deal.

(?<1>.*?) – Once you’ve found the opening tag, and ignored the optional stuff, grab the next bit and call it "1".

\s* – Again with the optional whitespace.

%> – This is our closing tag.

So this looks for anything in the format <%ABC%>, and pays close attention to the "ABC" part in particular, giving it a name so that I can talk about it later.

And that’s it, really.  It’s going straight into my core library for future use.  I’m not saying that this will run your enterprise, but for those times when you just need to fill in some blanks, this’ll do the trick.

UPDATE:
If you don’t want the tags to be case-sensitive, you can specify a comparer when instantiating the Dictionary.  Just change the constructor to this:

public TemplateParser()
{
    TagValues = new Dictionary<string, object>(StringComparer.CurrentCultureIgnoreCase);
}
Advertisement
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s