|
| Tue, May 13th | home | browse | articles | contact | chat | submit | faq | newsletter | about | stats | scoop | 12:17 PDT |
|
login « register « recover password « |
| [Article] | add comment | [Article] |
Templates are a valuable tool in any programmer's toolkit. I'm not talking about C++ templates, in which new concrete classes are created by replacing variable types within a template class. I'm talking about text templates, in which a string contains markers for replacement items, which are replaced with values. Copyright notice: All reader-contributed material on freshmeat.net is the property and responsibility of its author; for reprint rights, please contact the author directly. For example, a template might look like this:
In this case the "template" is the string, and the replacement tokens are "name" and "amount", which can be replaced on the fly with new values. What are templates used for?Templates are useful in a whole variety of scenarios. Here are a few to stoke the fire in your imagination:
Why not use HTML::Mason, JSP, or ASP?Serious page markup languages like HTML::Mason, JSP, and ASP are excellent for doing all of the above. However, it's still valuable to have a small templating system that matches smaller requirements and needs. Several different sizes of templating systems are shown below; you can pick the right style to fit your needs. Why use Ruby?First, because I like it. Second, because I want to give Ruby some more exposure. Third, Ruby has regular expressions built in. Regular expressions are at the core of any template system, since the idea is to replace special tags with values that are dynamically generated. Templating systems can be built in languages like C, C++, or Java, but you should look seriously into a regular expression library to make the implementation easier. Raw string parsing in C is no fun. A Note on FilesThe example code in this article uses template strings that are stored with the code to make understanding the examples easier. Most of the time, however, you will want to store a template in a local file so you can make alterations on it without changing the code.
To read a file into a string with a very short command, use:
Simple Template SystemsRuby itself has a sort of templating system built into it already; the string formatting system in Ruby can be used to do template replacements. The code below illustrates this:
We store the template in a string. The template uses the standard
Ruby inline replacement syntax for variables. The standard syntax is
Tweaking the Simple System
The #{ and } formatting syntax is rather Ruby-specific. You might
want to go with something that can be reused in different
environments. In the example below, I use ":::" as the delimiter, so
that tokens look like
Here, we replace A Regular Expressions ApproachFor a variety of reasons, you may not want to use eval for templates. In particular, if security is a concern, you want to avoid allowing arbitrary code to be evaluated. The example below shows a use of regular expressions to do replacement:
The fact that I used a function to make this happen is really irrelevant. The main work happens in the gsub call within the template function. The regular expression used with gsub finds the replacement tokens, gets the token name, and puts it in $1. The block attached to gsub is called when a match is found, and the result of the block is put back into the string. In this case, $1 is used as an index of the values hash to get the correct replacement value for the string. Now is probably a good time to stress that you should learn to love regular expressions (RegExps for short) if you haven't already. Once you get the hang of regular expressions, you will never want to go back to hand parsing text input. A Different Take on the RegExp ApproachThere are always many ways to do the same thing. To illustrate that in our current problem, here is another take on the idea:
In the previous implementation, we used the regular expression to
drive the search and to find the tags, which were then replaced by
using the interior value of the token as an index of a hash. In
this implementation, we reverse the flow. We use the array of keys
to drive the string replacement. The regular expression
My hunch is that this method is actually slower than the previous one, since in the previous example, the regular expression engine was only tasked with one set of replacements, so the string shuffling was kept to a minimum. All these functions, though... Isn't Ruby an Object Oriented language? The next section demonstrates an OO approach to templates. Text Templates As ObjectsTemplates work well as objects. Encapsulation allows them to be passed around as arguments to other classes and functions. Ruby makes that all very easy. This example is an initial pass at a class-based implementation:
The constructor takes the text string and successively calls the set method create associations between the replacement tokens and the values to replace them with. The run method then builds a new string from the template with the values replaced. The override of the to_s method is a convenience that allows the class to be converted to a string to force the evaluation. Since the template string stored within the object is not altered when run is invoked, the same template object can be used multiple times with different calls to give different results. Here's an example use of the class:
The class code is stored in a file named "Template.rb", so this test is requiring that file. The template is created with new, then the associations are made with the invocations of set, and the template is evaluated by the call to print. The print call invokes to_s, which in turn invokes run. Making Debugging EasierA common problem with text templating systems is a mismatch between the key name in the template and the key name in the code. To help diagnose this problem, you may want to add a runtime exception when the key found in the template has not been associated with anything in the object. The code below illustrates this:
Now the run code checks to make sure that the key found in the template is actually a value in the @values hash. If it's not, an exception is raised. Here is the updated test code that causes the exception as a test:
First, we try without having both "customer" and "amount" set. That fails, but when we try again with both set, it works. Finding errors is one thing. Making errors less frequent is good preventive medicine. One way to do that would be to coerce all of the key values to lower or upper case to prevent case-sensitive errors. That exercise is left to the reader. Methods and Functions as Replacement ValuesOne thing we lost in going from an eval system to a regular expression-based system was that the replacement tokens in an eval could be Ruby expressions that were more than just straight variable replacements. Sometimes, simple variable replacement isn't enough. You will often find yourself wishing for a simple expression within the replacement tag, or you may have a tag that has a complex syntax that doesn't easily fit into a simple key/value pairing. That's where this next example class comes in. In this class, the set of values can be either a hash or a method:
Notice that we removed the set method and replaced that functionality with a single extra value on the constructor. This value is either a hash or a method. Because Ruby is an excellent dynamic language, we are able to normalize both the hash and method cases to both be method calls. This works because we get a method reference to the fetch method of the hash, which has the same signature as our target method. It takes one string and returns one value. Also notice that the raise code is gone from the previous example, since it is no longer appropriate for all use cases. Test code for this class is shown below:
The first set of tests around the temp1 object test the hash-driven API. The second test around the temp2 object tests the method- or function-driven API by sending a method reference to the myItems function. I have to stand back for a second and say that this method referencing functionality is very impressive and a great feature of Ruby. What's important to understand is that by calling method on an object, you get a method reference to that method on that object. In C++, you can get a pointer to a member function, but would have to store the pointer to self separately. Method functionality in Ruby handles this for you. The object returned from the method code stores both the self reference and the method reference. This very cool, and yet another reason to love Ruby. Allowing Multiple Replacement Value SetsI haven't run into this very often, but I have seen cases in which you want different sets of replacement values. For example, you may have one set of replacements for constants and another for values. This last Template class example allows you to register several different tag sets to look for, which can be associated with either hash values or methods.
Now the constructor is back to taking a single format text string, and we have brought back the set method. In this case, though, the set method takes two values. The first specifies what will bracket the token name in the format, such as ":::" or "!!!" or whatever you like. The second is the hash or method to use when looking up the value for the replacement token. The run method in this class then uses this list of replacement sets to make successive groups of search and replace operations to turn the template string into the output string. Here's example code that uses this class:
The code is fairly simple. We build the template class with the template string, set up the replacement set with the set method, then evaluate the template with the replacement values using the to_s method (invoked by print). ConclusionsText templating is a powerful tool that should be in every programmer's back pocket. Often, I find that invoking the true power of a large-scale templating language is just too much for a particular problem. In these cases, simple text templating systems like the ones described in this article come in very handy. Special thanks to Dru Nelson for his help on this article. Author's bio: Jack Herrington is the editor of the Code Generation Network, a free information site dedicated to the dissemination of information about code generation techniques. His book, Code Generation In Action, will be released by Manning in July. He is the proud father of six month old Megan Michele. T-Shirts and Fame! We're eager to find people interested in writing articles on software-related topics. We're flexible on length, style, and topic, so long as you know what you're talking about and back up your opinions with facts. Anyone who writes an article gets a t-shirt from ThinkGeek in addition to 15 minutes of fame. If you think you'd like to try your hand at it, let jeff.covey@freshmeat.net know what you'd like to write about. [Comments are disabled]
[»]
Nice! Excellent! Thanks!
[»]
Good Very good tutorial. Has helped me alot :-)
[»]
good article I'd like to thak for this. I think, i've now ideas for ab bit contentmanaging..
[»]
Thanks. I'd just like to take a minute to thank you, Mr. Herrington, for this
excellent tutorial. I feel that I have so much more power right at my
finger tips. --
|