Приглашаем посетить
История (med.niv.ru)

Section 3.5.  Template Toolkit

Previous
Table of Contents
Next

3.5. Template Toolkit

While the solutions we've seen so far have been primarily for Perl programmers embedding Perl code in some other mediumAndy Wardley's Template Toolkit (http://www.template-toolkit.org/) is slightly different. It uses its own templating language to express components, loops, method calls, data structure elements, and more; it's therefore useful for teaching to designers who have no knowledge of the Perl side of your application[*] but who need to work on the presentation. As the documentation puts it, you should think of the Template Toolkit language as a set of layout directives for displaying data, not calculating it.

[*] And probably no desire to find out!

Like Mason, it seamlessly handles compiling, caching, and delivering your templates. However, unlike Mason, it's designed to provide general-purpose display and formatting capabilities in a very extensible way. As an example, you can use Template Toolkit to dynamically serve up PDF documents containing graphs based on data from a databaseand all this using nothing other than the standard plugins and filters and all within the Template Toolkit mini language.

But before we look at the clever stuff, let's look at the very simple uses of Template Toolkit. In the simplest cases, it behaves a lot like Text::Template. We take a template object, feed it some values, and give it a template to process:

    use Template;
    my $template = Template->new(  );
    my $variables = {
        who        => "Andy Wardley",
        modulename => "Template Toolkit",
        hours      => 30,
        games      => int(30*2.4)
    };
    $template->process("thankyou.txt", $variables);

This time, our template looks like the following:

    Dear [% who %],
        Thank you for the [% modulename %] Perl module, which has saved me
    [% hours %] hours of work this year. This would have left me free to play
    [% games %] games of go, which I would have greatly appreciated
    had I not spent the time goofing off on IRC instead.

    Love,
    Simon

Lo and behold, the templated text appears on standard output. Notice, however, that our variables inside the [% and %] delimiters aren't Perl variables with the usual type sign in front of them; instead, they're now Template Toolkit variables. Template Toolkit variables can be more than just simple scalars, though; complex data structures and even Perl objects are available to Template Toolkit through a simple, consistent syntax. Let's go back to our design work invoices, but with a slightly different data structure:

    my $invoice = {
        client => "Acme Motorhomes and Eugenics Ltd.",
        jobs => [
         { cost => 450.00, description => "Designing the new logo" },
         { cost => 300.00, description => "Letterheads and complements slips" },

         { cost => 900.00, description => "Web site redesign" },
         { cost =>  33.75, description => "Miscellaneous Expenses" }
        ],
        total => 0
    };

    $invoice->{total} += $_->{cost} for @{$invoice->{jobs}};

How would we design a template to fit that data? Obviously, we're going to need to loop over the jobs in the anonymous array and extract various hash values. Here's how it's done:

    To [% client %]:

    Thank you for consulting the services of Fungly Foobar Design
    Associates. Here is our invoice in accordance with the work we have
    carried out for you:

    [% FOREACH job = jobs %]
         [% job.description %]  : [% job.cost %]
    [% END %]

    Total                                            $[% total %]

    Payment terms 30 days.

    Many thanks,
    Fungly Foobar

As you can see, the syntax is inspired by Perlwe can foreach over a list and use a local variable job to represent each element of the iterator. The dot operator is equivalent to Perl's ->it dereferences array and hash reference elements and can also call methods on objects.

However, there's something slightly wrong with this example; since we can expect our descriptions to be of variable width, our costs aren't going to line up nicely at the end.[*] What can we do about this? This is where a nice, extensible feature of the Template Toolkit called filters comes in.

[*] We completely glossed over this in the Text::Template example; did you notice?

3.5.1. Filters

Template Toolkit filters are a little like Unix filtersthey're little routines that take an input, transform it, and spit it back out again. And just like Unix filters, they're connected to our template output with a pipe symbol (|).

In this case, the filter we want is the oddly named format filter, which performs printf-like formatting on its input:

    [% job.description | format("%60s") %]  : [% job.cost %]

This fixes the case where the data is being produced by our template processorjob.description is turned into a real description, and then filtered. But we can also filter whole blocks of template content. For example, if we wanted to format the output as HTML, we could apply the html_entity filter to replace entities with their HTML encoding:

    [% FILTER html_entity %]
    Payment terms: < 30 days.
    [% END %]

This turns into: Payment terms: &lt; 30 days.

This is another example of a Template Toolkit block; we've seen FOREACH blocks and FILTER blocks. There's also the IF/ELSIF/ELSE block:

    [% IF delinquent %]
       Our records indicate that this is the second issuing of this
    invoice. Please pay IMMEDIATELY.
    [% ELSE %]
       Payment terms: <30 days.
    [% END %]

Other interesting filters include the upper, lower, ucfirst, and lcfirst filters to change the casing of the text; uri to URI-escape any special characters; eval to treat the text to another level of template processing, and perl_eval to treat the output as Perl, eval it, and then add the output to the template. For a more complete list of filters with examples, see the Template::Manual::Filtersdocumentation.

3.5.2. Plugins

While filters are an interface to simple Perl functionalitybuilt-in functions like eval, uc, and sprintf, or simple text substitutionsplugins are used to interface to more complex functions. Typically, they're used to expose the functionality of a Perl module to the format language.

For instance, the Template::Plugin::Autoformat plugin allows one to use Text::Autoformat's autoformatting functionality. Just as with the Perl module, use the USE directive to tell the format processor to load the plugin. This then exports the autoformat subroutine and a corresponding autoformat filter:

    [% USE autoformat(right=78) %]
                                                    [% address | autoformat %]

This assures that the address is printed in a nice block on the right-hand side of the page.

A particularly neat plugin is the Template::Plugin::XML::Simple module, which allows you to parse an XML data file using XML::Simple and manipulate the resulting data structure from inside a template. Here we use USE to return a value:

    [% USE document = XML.Simple("app2ed.xml") %]

And now we have a data structure created from the structure and text of an XML document. We can explore this data structure by entering the elements, just as we did in "XML Parsing" in Chapter 2:

    The author of this book is
    [% document.bookinfo.authorgroup.author.firstname # 'Simon'  %]
    [% document.bookinfo.authorgroup.author.surname   # 'Cozens' %]

Actually writing a plugin module like this is surprisingly easyand, in fact, something we're going to need to do for our RSS example. First, we create a new module calledTemplate::Plugin::Whatever, where Whatever is what we want our plugin to be known as inside the template language. This module will load up whatever module we want to interface to. We'll also need it to inherit from Template::Plugin. Let's go ahead and write an interface to Tony Bowden's Data::BT::PhoneBill, a module for querying UK telephone bills.

    package Template::Plugin::PhoneBill;
    use base 'Template::Plugin';
    use Data::BT::PhoneBill;

Now we want to receive a filename when the plugin is USEd and turn that into the appropriate object. Therefore we write a new method to do just that:

    sub new {
        my ($class, $context, $filename) = @_;
        return Data::BT::PhoneBill->new($filename);
    }

$context is an object passed by Template Toolkit to represent the context we're being evaluated in. And that's basically ityou can add error checking to make sure the filename exists and that the module can parse the phone bill properly, but the guts of a plugin are as we've shown.

Now that we've created the plugin, we can access the phone bill just like we did with the XML::Simple data structure:

    [% USE bill = PhoneBill("mybill.txt") %]

    [% WHILE call = bill.next_call %]
    Call made on [% call.date %] to [% call.number %]...
    [% END %]

An interesting thing to notice is that when we were using the XML.Simple plugin, we accessed elements of the data structure with the dot operator: document.bookinfo and so on. In that case, we were navigating hash references; the Perl code would have looked like $document->{bookinfo}->{authorgroup}->{author}.... In this example, we're using precisely the same dot operator syntax, but, instead of navigating hash references, we're calling methods: call.date would translate to $call->date. However, it all looks the same to the template writer. This abstraction of the underlying data structure is one of the big strengths of Template Toolkit.

3.5.3. Components and Macros

When we looked at HTML::Mason, one of the things we praised was the ability to split template functionality up into multiple components, then include those components with particular parameters. It shouldn't be a surprise that we can do precisely the same in Template Toolkit.

The mechanism through which we pull in components is the INCLUDE directive. For instance, we can specify our box drawing library in a way very similar to the HTML::Mason method, as in Example 3-16.

Example 3-16. BoxTop

<table bgcolor="#777777" cellspacing=0 border=0 cellpadding=0>
<tr>
  <td rowspan=2></td>
  <td valign=middle align=left bgcolor="[% color %]">
    &nbsp;
   <font size=-1 color="#ffffff">
   <b>
   [% IF title_href %]
      <a href="[% title_href %]"> [% title %] </a>
   [% ELSE %]
      [% title %]
   [% END %]
   </b>
   </font>
  </td>
  <td rowspan=2>&nbsp;</td>
</tr>
<tr>
  <td colspan=2 bgcolor="#eeeeee" valign=top align=left width=100%>
  <table cellpadding=2 width=100%>
    <tr><td>

And in the same way as HTML::Mason, we can use local parameters when we include these components:

    [% INCLUDE boxtop
               title = "Login"
               ...
    %]

However, Template Toolkit provides another method of abstracting out common components, the MACRO directive. We can define a MACRO to expand to any Template Toolkit code; let's start by defining it to simply INCLUDE the drawing component:

    [% MACRO boxtop INCLUDE boxtop %]
    [% MACRO boxend INCLUDE boxend %]

With this, we can draw boxes with a little less syntax:

    [% boxtop(title="My Box") %]

       <P> Hello, people! </P>
    [% boxend %]

Instead of using a component file and INCLUDE, we can also associate a block of Template Toolkit directives with a macro name.

    [% MACRO boxtop BLOCK %]
    <table bgcolor="#777777" cellspacing=0 border=0 cellpadding=0>
    <tr>
      ...
    [% END %]

    [% MACRO boxend BLOCK %]
    </td></tr></table>

    </td></tr>
    <tr><td colspan=4>&nbsp;</td></tr>
    </table>
    [% END %]

Eventually, we can build up a library of useful macros and then INCLUDE that, instead of having a bunch of component files hanging around.

Let's assume we've created such a library and it contains these two box-drawing macros, and now we'll move on to putting together our RSS aggregator.

3.5.4. The RSS Aggregator

When it comes to writing the aggregator, we first look at the list of Template Toolkit plugins and notice with some delight that there's already a Template::Plugin::XML::RSS, which talks to XML::RSS. Unfortunately, our delight is short-lived, as we soon discover that this expects to get a filename rather than a URL or a string of XML data. We don't really want to be writing out files and then parsing them in again.

So let's create our own subclass of Template::Plugin::XML::RSS that fetches URLs and parses those instead:

    package Template::Plugin::XML::RSS::URL;
    use base 'Template::Plugin::XML::RSS';
    use LWP::Simple;

    sub new {
        my ($class, $context, $url) = @_;

        return $class->fail('No URL specified') unless $url;

        my $url_data = get($url)
          or return $class->fail("Couldn't fetch $url");

        my $rss = XML::RSS->new
          or return $class->fail('failed to create XML::RSS');

        eval { $rss->parse($url_data) } and not $@

        or return $class->fail("failed to parse $url: $@");

        return $rss;
    }

    1;

Now we can build up the equivalent of the RSSBox component we made in Mason:

    [% MACRO RSSBox(url) USE rss = XML.RSS.URL(url) %]
    [% box_top(title = rss.channel.title, title_href = rss.channel.link) %]

    <dl class="rss">
    [% FOREACH item = news.items %]
        <dt class="rss">
           <a href="[% item.link  %]"> [% item.title %] </a>
        [% IF full %]
           <dd> [% item.description %] </dd>
        [% END ]
        </dt>
    [% END %]
    </dl>
    [% box_end %]
    [% END %]

The important difference between this and the Mason example is that this piece of code handles everything itselfthe whole process of obtaining and parsing the RSS feed is available to the template designer. There's no Perl code here to be seen at all. It's also considerably more concise and easier to read and understand. Now that we have this macro, we can produce an HTML box full of RSS stories with a simple call to it:

    [% RSSBox("http://slashdot.org/slashdot.rss") %]

From here on, constructing an RSS aggregator is a simple matter of templating; all of the Perl work has been abstracted away.

    Previous
    Table of Contents
    Next