NAME

JF::Template - pure perl replacement for Text::Tmpl


SYNOPSIS

  use JF::Template;
  my $t = JF::Template->new;
  $t->set_value( name => "Jonathan");
  $t->set_value({
      one => 1,
      two => 2,
  });
  foreach my $l (qw(a b c d)) {
      my $loop = $t->get_loop("letters");
      $loop->set_value( letter => $l );
  }
  $t->set_dir( "/home/jfield/templates" );
  print $t->parse_file( "foo.html" );


USAGE

I've written the usage documentation from two angles: usage for people who never used this stuff before, and usage for people who know Text::Tmpl, which this module is more-or-less compatible with. Here we go...

Usage For Newbies

Working on it... for now go read the Text::Tmpl docs, then the section below.

Usage For Text::Tmpl Users

If you've used Text::Tmpl, this is very similar. Here are the difference, or, the top ten reasons to use this instead:

  1. The tag delimiters are done <% like this %>
  2. There is a new template command:
       <% set "foo", "bar" %>
     in the template is exactly equivalaent to:
       $t->set_value( foo => "bar" )
     in the script.  It takes precedence over
     the script values, as well.
  3. $t->set_dir() works with or without the trailing slash
  4. There is no ifn/endifn tag pair, instead you use an "!"
     before the argument like this: <% if ! $foo %> no foo <% endif %>
  5. There are elsif and else tags that work like you'd expect:
        <% if $foo %>
            FOO
        <% elsif $bar %>
            BAR
        <% else %>
            BAZ
        <% endif %>
  6. You can do simple logic in your if statements like this:
        <% if $foo && !($bar || $baz) %>
            HELLO
        <% endif %>
      ...but that's it!  we want to give the template owners some
      power, but let's keep the perl in the script!
  7. There are plenty of verbose warnings to help debug:
     like warnings when the directory or file can't be opened, and
     malformed/misnested template tags generate warnings with line
     numbers.
  8. set_value() takes either a single key/value pair or a hashref,
     and warns if you give it invalid arguments.  get_loop() is
     easier to type than loop_iteration()
  9. It uses less memory right up front, and it doesn't leak
 10. It's pure perl so you can hack it and see how it works.

The downside is that it can be slower in some cases, but read on...


PERFORMANCE

The only downside of this module in comparison to Text::Tmpl is that it's slower. In fact Text::Tmpl is about twice as fast. That seems like a lot, but given the improved memory usage and conveniences, it may be worth it unless you have an blinding need for speed and speed alone. 1000 reps on a G4 1.33mhz with a smallish template:

 david:  4 secs ( 2.50 usr +  1.16 sys =  3.66 CPU) @ 273.22/s (n=1000)
   jon:  7 secs ( 6.50 usr +  0.10 sys =  6.60 CPU) @ 151.52/s (n=1000)

Note that the specifics of the template have a huge effect on speed. Notably, the enhanced logic operators allowed in if/elsif arguments incur an eval, which causes a performance hit: worst case is 30% slower than simple args to if/elsif. ``Simple args'' in this case means a single variable, optionally preceded by ``!''. If you use any of ``( ) || &&'' then you incur the enhanced logic code. However, don't sweat it too much as some clever caching eliminates most of the performance hit in most cases -- especially inside loops and in subsequent calls in mod_perl processes; where it counts most.

The template used in the above benchmark does not use any enhanced logic. This is only fair since Text::Tmpl doesn't even support enhanced logic.

I have put a fair effort into optimizing, but it'll never be as fast as Text::Tmpl -- seems the majority of time is spent in _render_tokens() which does a lot of recursing, and C functions recurse faster than perl's. Heck I even tried the crazy &_func notation to avoid repassing @_, but couldn't get it any faster. I also tried a weird method where I contiually spliced tokens onto the token array as we rendered instead of recursing -- still no win. _tokenize_file() is where the next most time is spent. The main thing seems to be that there's no good way to tokenize without copying the string bit by bit. In C you can just point to the template strings directly from the tokens. I've tried tricks with split() and pos() and whatnot, but this is as good as I could get it in perl.

At this point I've tried about everything I can think of. Feel free to offer suggestions, though, other than rewriting it in C ;)

Here's some data for memory usage; note the RSIZE and VSIZE numbers...

               #MREGS  RPRVT  RSHRD  RSIZE  VSIZE

JF::Template 500 reps 22 1.13M 404K 1.39M 26.8M 1000 reps 22 1.13M 404K 1.39M 26.8M 2000 reps 22 1.13M 404K 1.39M 26.8M

Text::Tmpl 500 reps 26 5.57M 544K 5.89M 28.2M 1000 reps 28 10.1M 544K 10.4M 30.2M 2000 reps 31 19.2M 544K 19.5M 40.3M

So basically, a bit better than half the speed, but with a much better memory footprint, no memory leaks, enhanced logic operators and better debugging output. A more than fair trade, says I.

Performance Secret

Okay, so in an effort to be humble and realistic, I've not been totally honest. The fact is that in practice, this module is actually faster than Text::Tmpl. This is achieved by caching the results of the tokenizing stage for each template. This means that the majority of the work is done only once for a template the first time it is used, and any subsequent calls to parse_file() will do much less work. Since the most common use for this module is to run under an Apache webserver dishing out page after page of the same template, it's actually a big win. Text::Tmpl re-tokenizes the template on every call. Here is a comparison:

  # 150 iterations of the same template
  Text::Tmpl   :   95.54/sec
  JF::Template :  147.06/sec

So, if the same template is likely to be reused before the process shuts down, you can actually see quite a speedup with this module over Text::Tmpl. The first call is a bit slower, and all subsequent calls are a bit faster.

The system is smart enough to pick up new files when they change.

This caching is turned on by default, and I haven't come across a good reason not to use it. It does use more memory, but unless you've got an absolutely huge template collection, it's negligable. Even if you do, it's probably still worth it. But a constant is provided at the top of the module to disable it if you want: just set USE_TOKEN_CACHE to ``0''.


NOTES

I think it's neat that something this useful is under 500 lines of actual code. And that it took me so many hours (spread over so many months) to get it all working smoothly. I am quite proud of this module :)

I should come up with some method to make it easier for template writers to detect misnested tags.

I should expand the tests and clean up the code a bit.

I should call my family more often.


DEPENDENCIES

None.


AUTHOR

Jonathan Field - jfield@gmail.com

...but based on the concepts of Neil Mix and David Lowe. Oh, and Dave Bailey wrote the cool export_*() functions!