Skip to content

Building a STS (Simple Template System)

September 14, 2009
tags:

Marek Foss recently posted a Simple Template System in Perl article on his f055: lazy design & development blog.

This got my brain buzzing and had a few hours to kill. So I decided to stretch my Perl foo & memory to build a STS from scratch to pass the time.

Here was the first prototype:

#!/usr/local/bin/perl -T

use strict;
use warnings;
use CGI ();

sub transform {
    my ($template, $vars) = @_;
    eval {
        no warnings;
        $template =~ s/<!--(.*?)-->/$vars->{$1}/g;
    };
    return $template;
}

sub internal_template {
    my $tmpl;
    $tmpl.= $_ for <DATA>;
    return $tmpl;
}

print CGI::header( -type => 'text/html' );

print transform internal_template() => {
    sidebar => 'This is a menu',
    content => 'This is the main content block',
};


__DATA__
<html>
<head>
<title>Simple Template System</title>
</head>
<body>
<div class=“menu”>
<!--sidebar-->
</div>
<div class=“main”>
<!--content-->
</div>
</body>
<!--baz-->
</head>

I’m using a hashref to pass the template variables. This is more flexible and allows me to use just the one substitution to transform all the template placeholders:

$template =~ s/<!--(.*?)-->/$vars->{$1}/g;

The eval & “no warnings” around this cover issues like calling an unknown template variable (ie. undefined hash key). For eg. see <!–baz–> silently disappears from output.

I am also using an internal_template() where the template HTML follows the __DATA__ line. Nice for simple CGI scripts but nothing stopping us having external_template() either.

If you require more than one internal template then:

#!/usr/local/bin/perl -T

use strict;
use warnings;
use CGI ();

sub transform {
    my ($template, $vars) = @_;
    eval {
        no warnings;
        $template =~ s/<!--(.*?)-->/$vars->{$1}/g;
    };
    return $template;
}

sub internal_template {
    my $show = shift;
    my %tmpl;
    my $template_name = 'main';  # default (1st) template
    
    for my $line ( <DATA> ) {
        if ( $line =~ m/^\*\*\*\s(\S+)\s\*/ ) {
            $template_name = $1;
            next;
        }
        $tmpl{ $template_name }.= $line;
    }
    return exists $tmpl{ $show } ? $tmpl{ $show } : $tmpl{ main };
}

print CGI::header( -type => 'text/html' );

print transform internal_template() => {
    sidebar => 'This is a menu',
    content => 'This is the main content block',
};

__DATA__
<html>
<head>
<title>Simple Template System</title>
</head>
<body>
<div class=“menu”>
<!--sidebar-->
</div>
<div class=“main”>
<!--content-->
</div>
</body>
</head>
*** nomenu *************************************************************
<html>
<head>
<title>Simple Template System (nomenu)</title>
</head>
<body>
<div class=“main”>
<!--content-->
</div>
</body>
</head>

Now there is a separator between each template (*** template_name ***). internal_template() now takes an optional template name (internal_template(‘nomenu’) would produce the alternate template in above code).

So what would a STS need next?

Well having functions passed to the template that can be called could be handy. This can be achieved by preceding an additional text substitution in the transform() sub:

$template =~ s//@{[ $vars->{$1}->( split ',', $2) ]}/g;

This code picks up placeholders with parenthesises and passes the parameters given to the specified function name. The @{[ … ]} does the extra bit of interpolation work needed for this to work.

Thus we can create function placeholders like this:

print transform internal_template('main') => {
    gbp       => sub { sprintf "£%.2f", $_[0] },
    li_helper => sub { map { qq{<li>$_</li>} } @_ },
};

using a template like this:
<p>Great British Pounds: <!--gbp(200)--></p>
<ul><!--li_helper(one,two,three)--></ul>

would produce the following:

<p>Great British Pounds: £200.00</p>
<ul><li>one</li> <li>two</li> <li>three</li></ul>

OK so far so good. But what if I wanted to call a Perl function without having to pre-define anything in script? Well we could precede the following substitution into transform() sub:

$template =~ s/<!--perl(.*?)-->/@{[ eval "$1" ]}/gs;

Which means you then could do something like this:
<p>Random number between 1 and 9: <!--perl int( rand(10) ) --></p>

Well this is all nice and dandy if all we want is the expression results returned. What if we wanted to use control statements that effect the output? ie. something like this in the template:

if ( int(rand(2)) ) {
    print '<ol>';
    for my $numb (1..5) {
        printf('<li>%d * 2 = %d</li>', $numb, $numb * 2);
    }
    print "</ol>\n";
}
else { print "</p>Nothing to show at this moment in time</p>" }

We’re starting to play with fire now! This can be achieved with a bit more control of the output stream so I created a process() subroutine to handle that.

Below is the full program with everything in bar the kitchen sink:

#!/usr/local/bin/perl -T

use strict;
use warnings;
use CGI ();

sub transform {
    my ($template, $vars) = @_;
    eval {
        no warnings;
        $template =~ s/<!--perl(.*?)-->/@{[ eval "$1" ]}/gs;
        $template =~ s/<!--(.*?)\((.*?)\)-->/@{[ $vars->{$1}->( split ',', $2) ]}/g;
        $template =~ s/<!--(.*?)-->/$vars->{$1}/g;
    };
    return $template;
}

sub process {
    my $tmpl = shift;
    my $vars = shift;
    my $buffer;
    
    my $start = qr{<\?perl>};
    my $end   = qr{</\?>};

    for (split /\n/, transform( $tmpl, $vars)) {
        if ( /$start/ .. /$end/ ) {
            if    ( /$start/ ) { $buffer = '' }
            elsif ( /$end/   ) { 
                eval $buffer;
                $buffer = '';  
            }
            else { $buffer.= $_ . "\n" }
            next;
        }
        print $_, "\n";
    }
}

sub internal_template {
    my $show = shift;
    my %tmpl;
    my $template_name = 'main';
    
    for my $line ( <DATA> ) {
        if ( $line =~ m/^\*\*\*\s(\S+)\s\*/ ) {
            $template_name = $1;
            next;
        }
        $tmpl{ $template_name }.= $line;
    }
    return exists $tmpl{ $show } ? $tmpl{ $show } : $tmpl{ main };
}

print CGI::header( -type => 'text/html' );

process internal_template('main') => {
    sidebar => 'This is a menu',
    content => 'This is the main content block',
    gbp     => sub { sprintf "£%.2f", $_[0] },
    li_helper => sub { map { qq{<li>$_</li>} } @_ },
};


__DATA__
<html>
<head>
<title>Simple Template System</title>
</head>
<body>
<div class=“menu”>
<!--sidebar-->
</div>
<div class=“main”>
<p>Great British Pounds: <!--gbp(200)--></p>
<ul><!--li_helper(one,two,three)--></ul>
<p>Random number between 1 and 9: <!--perl int( rand(10) ) --></p>
<?perl>
    if ( int(rand(2)) ) {
        print '<ol>';
        for my $numb (1..5) {
            printf('<li>%d * 2 = %d</li>', $numb, $numb * 2);
        }
        print "</ol>\n";
    }
    else { print "</p>Nothing to show at this moment in time</p>" }

</?>
<!--content-->
</div>
</body>
<!--baz-->
</head>

process() simply loops thru the fulfilled template printing out each line as it goes along except when hitting a <?perl> block which it evals instead.

Above isn’t perfect but it does exactly what it says on the tin. Well that is it use to before I bogged it down with features. I don’t think we can call it a Simple Template System anymore? How about “Super”? “Spectacular”? or even “Special”? Perhaps another word beginning with “S” maybe more appropriate 😉

They say writing a template system is a rite of passage for every Perl programmer. Its been a while since I did my last one so perhaps its also some kind of recursive itch we have?

Anyway I’ll stop procrastinating and return to using Template Toolkit for my templating needs 😉

/I3az/

3 Comments leave one →
  1. September 15, 2009 10:56 am

    I’m glad you grabbed my concept 😉 I really like the hashref idea to simplify regex, but the rest is not a Simple approach at all 😉

    • September 15, 2009 11:11 am

      re: hashref – It also produces a nicer API as well as the cuter regex.

      re: “rest is not a Simple approach” – Definitely not simple anymore 🙂 As u probably gathered the exercise was to show how easy it was to get from KISS to bloated via feature creep!

      • September 15, 2009 11:47 am

        I’ve updated my post to reflect your hashref idea. Thanks !

        re: bloated – well, i’ve received a comment on dzone dissing the idea for being just a simple search/replace. Why people expect it all to be complicated beyond use ? That just gives me an idea for another post, or a rant rather 😉

Leave a comment