Sunday, October 10, 2010

Updated Cut-n-paste Templating Code

I updated my perl cut-n-paste templating code.

  • No modules if you cutnpaste
  • All in one method
  • Draws from a Hash-of-things
  • Interpolate
  • Iterate
  • No "if", no "include
  • No hash-of-hashes

Fixes: some bugs, sigils can be escaped, array element access.

And revised it a bit. Now 33 lines of perl (not counting comments/blank-lines).

Get it from github (you must right-click/download or you'll just get it displayed): cutnpaste_template.pm

You'll see that the file has some extra stuff in it. You can run it to process input as a template, run it to process the example template, or "use" it and call render(...).

The __DATA__ template in the github file documents the full usage/behavior.

Example

Assuming data:
%data = (
simple => "some text",
iterate => [ { name => "Amy", pet => "Dog" }, { name => "Bob", pet => "Cat" } ],
);

Here's a simple example template:

Interpolate $simple
@iterate[I'm $name, I like $pet.]

Is there a way to use Text::Balanced to parse out the @word[...] chunks? That would make the code more compact.

Here it is, but I think I'll stop posting the source and let you get it from github from now on:

# version 2: supports escaping the sigils
# version 3: supports catenation, $_ in iteration, and $0..$9 for arrays

use strict; use warnings; no warnings 'uninitialized'; use Text::Balanced qw(extract_bracketed); sub render { my ($template, $data) = @_; my @rez; # 1st iteration: head@name[block]rest... # $template = "rest" for next split # Split gives just "head" if no "@", so processes each piece # avoid $' with a split # We actually split on \@ or @, so we can remove the \ while (my ($head, $escape, $field_name, $block) = split(/(\\?)@([a-zA-Z]\w*|_)(?=\[)/, $template,2)) { # this was a \@word[..., so remove the \ if ($escape) { $head = $head."@".$field_name; } # fix bracket escapes $head =~ s/\\\[/[/g; $head =~ s/\\\]/]/g; # interpolate scalars in the "head" # Have to capture the \ so we can do the right thing my $interp = sub { # escape, fieldname if ($1) { '$'.$2 } # escaped, so remove / elsif (index('0123456789',$2) >= 0) { $data->{'_'}->[$2]; } else { $data->{$2}; } # interp }; $head =~ s/(\\?)\$([a-zA-Z]\w*|_|[0-9])/&$interp/eg; push @rez, $head; last if ! $field_name; # this was a \@word[..., so next on ... if ($field_name =~ /^\\(.+)/) { $template = $block; next; } # Get the "block" and "rest" my $bracketed; ($bracketed, $template) = extract_bracketed( $block, '['); $bracketed =~ s/^\[//; $bracketed =~ s/\]$//; # Repeat the block my $list = (ref($data->{$field_name}) eq 'ARRAY') ? $data->{$field_name} : [$data->{$field_name}]; foreach my $sub_data ( @$list ) { # recurse on this block with our block's data push @rez, render( $bracketed, {%$data, (ref($sub_data) eq 'HASH' ? %$sub_data : ()), '_' => $sub_data}); } } join("",@rez); }

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;
}