Tom Gracey
and 1 contributors

NAME

Template::Nest - manipulate a generic template structure via a perl hash

SYNOPSIS

        page.html:
        <html>
                <head>
                        <style>
                                div { 
                                        padding: 20px;
                                        margin: 20px;
                                        background-color: yellow;
                                }
                        </style>
                </head>

                <body>
                        <% contents %>
                </body>
        </html>
         


        box.html:
        <div>
                <% title %>
        </div>


        use Template::Nest;

        my $page = {
                NAME => 'page',
                contents => [{
                        NAME => 'box',
                        title => 'First nested box'
                }]
        };

        push @{$page->{contents}},{
                NAME => 'box',
                title => 'Second nested box'
        };

        my $nest = Template::Nest->new(
                template_dir => '/html/templates/dir'
        );

        print $nest->render( $page );
  
        
        # output:

    <html>
            <head>
                    <style>
                            div { 
                                    padding: 20px;
                                    margin: 20px;
                                    background-color: yellow;
                            }
                    </style>
            </head>

            <body>          
            <div>
                    First nested box
            </div>
            <div>
                    Second nested box
            </div>
            </body>
    </html>

DESCRIPTION

This is HTML::Template::Nest, but the dependency on HTML::Template is dropped, and the module is made generic (ie not specific to HTML) for the following reasons:

  1. Given HTML::Template::Nest only uses the TMPL_VAR parameter from HTML::Template, hauling around the rest of HTML::Template is unnecessary baggage

  2. There's no reason to restrict this to HTML either in name or function - this is a system of combining templates, which can be of any arbitrary format

Let me take a moment to explain why I think Template::Nest is the only templating system that makes any sense.

The description for Text::Template says the following in the Philosophy section:

"When people make a template module like this one, they almost always start by inventing a special syntax for substitutions. For example, they build it so that a string like %%VAR%% is replaced with the value of $VAR. Then they realize the need extra formatting, so they put in some special syntax for formatting. Then they need a loop, so they invent a loop syntax. Pretty soon they have a new little template language.

This approach has two problems: First, their little language is crippled. If you need to do something the author hasn't thought of, you lose. Second: Who wants to learn another language? You already know Perl, so why not use it?"

These paragraphs agree with the philosophy of Template::Nest, in that you shouldn't need to invent a new language to fill in templates. However, the Text::Template description continues:

"Text::Template templates are programmed in Perl. You embed Perl code in your template, with { at the beginning and } at the end."

At this point the philosophy behind Text::Template and Template::Nest part ways. In the Template::Nest philosophy templates are not "programmed" at all. There should never be any code "embedded" in any template. Furthermore it is not a template if it has processing embedded in it.

The Template::Nest philosophy has the following:

  1. Templates are nothing other than dull, inanimate pieces of text with holes in them to fill in, similar to children's colouring templates

  2. Templates have no intelligence and are not capable of formatting text, testing conditions or performing loops. This stuff is control processing and should not occur in the template. Given that you already have text formatting, conditional processing and loops in the body of your code, why have any in your "template"? And how do you decide which of this processing goes in your template and which in the body of your code?

  3. By virtue of the above points, templates should be language independent. They should not care which language is used to fill them in. You should be able to port your template library over from perl to python to java etc. without needing to rewrite anything in the library, such that they combine together in the same way to give the original output.

I suggest that there is only one way to solve the problem which adheres to points 1 to 3 (above). The overall template structure must be provided to a builder which recursively fills in the template variables from the outside.

Just like a database, there must be only one particular "schema" of templates which suits a given situation (it must satisfy similar logical rules, such as "one table has many table rows" etc). And there is no room for variation within the templates, since they contain no processing. Thus there is only one way to solve the problem.

Thus Template::Nest is the only templating system which makes sense.

  • Specify the structure including conditionals, loops, formatting in the code.

  • Create all the templates that are needed so they can be repeated where necessary and filled in recursively

An example

Lets say you have a template for a letter (if you can remember what that is!), and a template for an address. Using HTML::Template you might do something like this:

    # in letter.html

    <TMPL_INCLUDE NAME="address.html">

    Dear <TMPL_VAR NAME=username>

    ....

However, in Template::Nest there's no such thing as a TMPL_INCLUDE, there are only tokens to fill in, so you would have

    # letter.html:

    <% address %>

    Dear <% username %>

    ...

I specify that I want to use address.html when I fill out the template, thus:

    my $letter = {
        NAME => 'letter',
        username => 'billy',
        address => {
            NAME => 'address', # this specifies "address.html" 
                               # provided template_ext=".html"
            
            # variables in 'address.html'
        }
    };

    $nest->render( $letter );

This is much better, because now letter.html is not hard-coded to use address.html. You can decide to use a different address template without needing to change the letter template.

Commonly used template structures can be labelled (main_page etc.) stored in your code in subs, hashes, Moose attributes or whatever method seems the most convenient.

Another example

The idea of a "template loop" comes from the need to e.g. fill in a table with an arbitrary number of rows. So using HTML::Template you might do something like:

    # in the template

    <table>
        <tr>
            <th>Name</th><th>Job</th>
        <tr>

        <TMPL_LOOP NAME=EMPLOYEE_INFO>
            <tr>
                <td><TMPL_VAR NAME=NAME></td>
                <td><TMPL_VAR NAME=JOB></td>
            </tr>
       </TMPL_LOOP>
    </table>

    # in the perl 

    $template->param(
        EMPLOYEE_INFO => [
            {name => 'Sam', job => 'programmer'}, 
            {name => 'Steve', job => 'soda jerk'}
        ]
    );
    print $template->output();

    # output 

    <table>

        <tr>
            <th>Name</th><th>Job</th>
        </tr>
        <tr>
            <td>Sam</td><td>programmer</td>
        </tr>
        <tr>
            <td>Steve</td><td>soda jerk</td>
        </tr>

    </table>

That's great - but why have the loop inside the template? If the table row is going to be repeated an arbitrary number of times, doesn't it make sense that this row should have its own template? In the Template::Nest scheme, this would look like:

    # table.html:

    <table>
        <tr>
            <th>Name</th><th>Job</th>
        </tr>

        <!--% rows %-->

    </table>


    # table_row.html:

    <tr>
        <td><!--% name %--></td>
        <td><!--% job %--></td>
    </tr>


    # and in the Perl:

    my $table = {
        NAME => 'table',
        rows => [{
            NAME => 'table_row',
            name => 'Sam',
            job => 'programmer'
        }, {
            NAME => 'table_row',
            name => 'Steve',
            job => 'soda jerk'
        }]
    };

    my $nest = Template::Nest->new(
        token_delims => ['<!--%','%-->']
    );

    print $nest->render( $table );

Now the processing is entirely in the Perl. Of course, if you need to fill in your table rows using a loop, this is easy:

    my $rows = [];

    foreach my $item ( @data ){

        push @$rows, {
            NAME => 'table_row',
            name => $item->name,
            job => $item->job
        };

    }

    my $table = {

        NAME => 'table',
        rows => $rows

    };

    my $nest = Template::Nest->new(
        token_delims => ['<!--%','%-->']
    );

    print $nest->render( $table );

Template::Nest is far simpler, and makes far more sense!

Some differences from HTML::Template::Nest

  • Template::Nest introduces the ability to specify templates via a hashref, rather than assuming templates are stored in files in a specific directory. This could be useful if your templates are defined programmatically, or extracted from database fields etc. See the template_hash method for more information.

  • Template::Nest allows the tokens (to be replaced) to be specified in arbitrary format. ie. you can have tokens of format <% token_name %>, [% token_name %] - or whatever token delimiters suit your project.

  • The ugly TMPL_VAR format of token used in HTML::Template is abandoned - it is unnecessary since the other TMPL_IF, TMPL_LOOP etc. token types are not used. Token delimiters now default to the mason style delimiters (<% and %>) - but any arbitrary token delimiters can be set - see token_delims.

  • Some minor renaming has taken place. In Html::Template::Nest there were comment_tokens. Moving forward I want to refer to tokens as the things being replaced in the template. So comment_tokens has become comment_delims (and token_delims has been introduced as above).

  • the method to_html has been replaced by render since Template::Nest does not specifically deal with html any more

METHODS

new

constructor for a Template::Nest object.

    my $nest = Template::Nest->new( %opts );

%opts can contain any of the methods Template::Nest accepts. For example you can do:

    my $nest = Template::Nest->new( template_dir => '/my/template/dir' );

or equally:

    my $nest = Template::Nest->new();
    $nest->template_dir( '/my/template/dir' );

name_label

The default is NAME (all-caps, case-sensitive). Of course if NAME is interpreted as the filename of the template, then you can't use NAME as one of the variables in your template. ie

    <% NAME %>

will never get populated. If you really are adamant about needing to have a template variable called 'NAME' - or you have some other reason for wanting an alternative label point to your template filename, then you can set name_label:

    $nest->name_label( 'GOOSE' );

    #and now

    my $component = {
        GOOSE => 'name_of_my_component'
        ...
    };

show_labels

Get/set the show_labels property. This is a boolean with default 0. Setting this to 1 results in adding comments to the output so you can identify which template output text came from. This is useful in development when you have many templates. E.g. adding

    $nest->show_labels(1);

to the example in the synopsis results in the following:

    <!-- BEGIN page -->
    <html>
        <head>
            <style>
                div { 
                    padding: 20px;
                    margin: 20px;
                    background-color: yellow;
                }
            </style>
        </head>

        <body>
            
    <!-- BEGIN box -->
    <div>
        First nested box
    </div>
    <!-- END box -->

    <!-- BEGIN box -->
    <div>
        Second nested box
    </div>
    <!-- END box -->

        </body>
    </html>
    <!-- END page -->

What if you're not templating html, and you still want labels? Then you should set comment_delims to whatever is appropriate for the thing you are templating.

token_delims

Get/set the delimiters that define a token (to be replaced). token_delims is a 2 element arrayref - corresponding to the opening and closing delimiters. For example

    $nest->token_delims( '[%', '%]' );

would mean that Template::Nest would now recognise and interpolate tokens in the format

    [% token_name %]

The default token_delims are the mason style delimiters <% and %>. Note that for HTML the token delimiters <!--% and %--> make a lot of sense, since they allow raw templates (ie that have not had values filled in) to render as good HTML.

comment_delims

Use this in conjunction with show_labels. Get/set the delimiters used to define comment labels. Expects a 2 element arrayref. E.g. if you were templating javascript you could do:

    $nest->comment_delims( '/*', '*/' );
    

Now your output will have labels like

    /* BEGIN my_js_file */
    ...
    /* END my_js_file */

You can set the second comment token as an empty string if the language you are templating does not use one. E.g. for Perl:

    $nest->comment_delims([ '#','' ]);

template_dir

Get/set the dir where Template::Nest looks for your templates. E.g.

    $nest->template_dir( '/my/template/dir' );

Now if I have

    my $component = {
        NAME => 'hello',
        ...
    }

and template_ext = '.html', we'll expect to find the template at

    /my/template/dir/hello.html

Note that if you have some kind of directory structure for your templates (ie they are not all in the same directory), you can do something like this:

    my $component = {
        NAME => '/my/component/location',
        contents => 'some contents or other'
    };

Template::Nest will then prepend NAME with template_dir, append template_ext and look in that location for the file. So in our example if template_dir = '/my/template/dir' and template_ext = '.html' then the template file will be expected to exist at

/my/template/dir/my/component/location.html

Of course if you want components to be nested arbitrarily, it might not make sense to contain them in a prescriptive directory structure.

template_ext

Get/set the template extension. This is so you can save typing your template extension all the time if it's always the same. The default is '.html' - however, there is no reason why this templating system could not be used to construct any other type of file (or why you could not use another extension even if you were producing html). So e.g. if you are wanting to manipulate javascript files:

    $nest->template_ext('.js');

then

    my $js_file = {
        NAME => 'some_js_file'
        ...
    }

So here HTML::Template::Nest will look in template_dir for

some_js_file.js

If you don't want to specify a particular template_ext (presumably because files don't all have the same extension) - then you can do

    $nest->template_ext('');

In this case you would need to have NAME point to the full filename. ie

    $nest->template_ext('');

    my $component = {
        NAME => 'hello.html',
        ...
    }

render

Convert a template structure to output text. Expects a hashref containing hashrefs/arrayrefs/plain text.

e.g.

    widget.html:
    <div class='widget'>
        <h4>I am a widget</h4>
        <div>
            <!-- TMPL_VAR NAME=widget_body -->
        </div>
    </div>


    widget_body.html:
    <div>
        <div>I am the widget body!</div>    
        <div><!-- TMPL_VAR NAME=some_widget_property --></div>
    </div>


    my $widget = {
        NAME => 'widget',
        widget_body => {
            NAME => 'widget_body',
            some_widget_property => 'Totally useless widget'
        }
    };


    print $nest->render( $widget );


    #output:
    <div class='widget'>
        <h4>I am a widget</h4>
        <div>
            <div>
                <div>I am the widget body!</div>    
                <div>Totally useless widget</div>
            </div>
        </div>
    </div>

SEE ALSO

HTML::Template::Nest HTML::Template Text::Template

AUTHOR

Tom Gracey tomgracey@gmail.com

COPYRIGHT AND LICENSE

Copyright (C) 2018 by Tom Gracey

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.20.1 or, at your option, any later version of Perl 5 you may have available.