Thursday, July 15, 2010

Cut-n-paste Templating Code

I needed a templating engine for a Perl script, but I couldn't install any modules, and I needed everything in one file. So, I wrote one in about 40 lines of Perl. Suitable for cut-n-paste into other projects.

See the updated version.

It only has to be really simple. Normally I hate having to construct a hash-of-hash-of-hashes for template engines, but the data model was also simple.

  • hash-of-hash data input
  • scalar interpolation
  • iterate on a list
  • No "if," no "include"
  • No escaping/quoting of interpolated values
  • No literal $
  • Efficient? Not very. For non-large templates.
  • Safe? Not very: interpolated values could get re-processed.

Key technology: Perl 5.10 and Text::Balanced::extract_bracketed.

use strict; use warnings; no warnings 'uninitialized';
use Text::Balanced qw(extract_bracketed);
sub render {
my ($template, $data) = @_;
# replace $x with $data->{'x'}
# replace @x[...] 
#    with foreach my $x (@{$data->{'x'}}) { ... }

# repeats, 1st, recurse
my @rez;
# avoid $' with a split
while (my @repeats = split(/@(\w+)(?=\[)/, $template,2)) {
    push @rez, $repeats[0];
    last if ! $repeats[1];
    my $field_name = $repeats[1];
    # warn "During '\@$field_name'";
    my $bracketed;
    ($bracketed, $template) = extract_bracketed( $repeats[2], '[');
    $bracketed =~ s/^\[//;
    $bracketed =~ s/\]$//;
    # warn "To repeat '\@$field_name', ".@{$data->{$field_name}}." times";
    foreach my $sub_data ( @{$data->{$field_name}} ) {
        # recurse on this block with our block's data
        push @rez, render( $bracketed, {%$data, %$sub_data});
        }
    # warn "Finished '\@$field_name'";
    }

my $rez = join("",@rez);

# scalars
$rez =~ s/\$(\w+)/$data->{$1}/eg;

return $rez;
}