This is a module to expand a file or string in the template syntax described below using supplied token values and handler functions. The primary motivation for this tool was to allow different template files to be used for the same underlying data so that the look of some web pages could be changed, but another good reason for using this is to make the form of such pages more easily editable without changing the content.
The intent is general purpose expansion of text files, rather than targetting a particular output format.
The template expander understands the following constructions:
$name$
$colour$
Any token which does not have a value for substitution will produce a null string.
token setting - values can be given to tokens. Most token values will be supplied by the code calling Template, but there may be cases where a special value needs to be set (as a default for instance).Setting token values is achieved using the [set] construct. This has
the form -
[set(<token>):<text>]
This assigns the expanded value of <text> to the specified
token.
Eg
[set(move_count):40]
[set(minor_deity):Rob]
The usual context for the token to be set is the local context, although a
token with the prefix GLOBAL_ will be set at the top level context.
The [set] construct does not produce any text output.
[ifdef(<token>):<text>]
[ifndef(<token>):<text>]
A token is considered to be defined if it has any non-null value, and
undefined if it has a null value. The result of the conditional
construct is the expanded value of the <text> argument if the
condition is satisfied.
[eval:<text>]
The text is passed directly to the Perl eval function, and the result
returned is the value of the construct. Any runtime error causes eval to
return a null string, the token evalerr being set to the error message.
Note that this function can be used to process token values under template control, but there is also a potential security risk if that is done. Care should be exercised.
The result of the eval construct is the value of the
<text> argument having been executed by the Perl eval
command.
Eg - to output the date the template is expanded:
[eval:
use POSIX;
use POSIX qw(strftime);
POSIX::strftime( "%d-%B-%Y", gmtime());
]
Note that code snippets for evaluation must be clean under strict.
loops - Template supports named and optionally constrained loops or sets of rows. These are termed clumps.A clump construct is of the form -
[clump(<count token>)<name>:<text>]
When Template encounters a clump, it looks for row data for a clump of the
given <name> in the current context: row data is an ordered set of
rows (that is, an array of token hashes) each element of which is used in turn
as the token context for the clump <text>. The row data will be
truncated according to the current value of the count token.
If no pre-computed row data is present for the clump, then Template looks for a
clump handler for the given <name>: this clump handler is expected
to return the row data as an array reference given the current token context
and the value of the specified count token (the clump handler should not return
more rows than this count value).
v0.3: Note that the interface to clump handlers has changed from previous versions. The earlier convention of expecting an actual array rather than an array reference made it very difficult for Template to work out whether the returned row data was empty, which meant that a clump text would always be expanded once with an empty token hash. For backwards compatibility, the previous interface will be looked for so old clump handlers should still work, but this is a deprecated feature which will be removed in a future revision.
Once these rows have been obtained, Template expands the given
<text> using the tokens for each row. In addition, the
following special tokens are used -
first - defined on the first row
last - defined on the last row
rowNumber - defined on each row, a 1-based row index.
The result of the [clump] is the concatenation of the expanded
<text> for all the rows returned by the clump handler. If no
rows are returned, no expansion is carried out.
Eg
[clump()totals:$total$
]
[clump()daysOfWeek:[ifdef(first):(]$day$[ifndef(last):,][ifdef(last):)]]
[clump(listLength)list:$listElement$
]
It is worth bearing in mind that because the result of a clump is dynamically generated at the time when the template is expanded, any kind of content which uses run time arguments can be embedded within a clump and its actual value determined by a clump handler. Also, although clumps are capable of dealing with many rows, there's no requirement that a clump consist of more than one.
So in the following example, the name of the game might be recovered by the clump handler from a game data file, but there will only ever be one row in the clump:
<h1>[clump()game:$gameName$]</h1>
Clumps may be nested. Tokens which are not found in the current clump context
are looked for in all the containing clump contexts (first and last are
the exceptions: if they are unresolved in the local context, no further search
is made). Since row data is effectively another form of token, it will also be
looked for in the stack of token contexts.
The requirements on clump handlers (arguments and so on) are discussed in more detail as part of the constructor arguments.
Inclusion of other templates. Template allows other templates to be inserted and expanded in situ.An include construct is of the form -
[include(<filename>)]
The specified file is read in and expanded at the site of the include directive, the expansion inheriting the token context of the include construct. The result of the include construct is thus the expanded text of the template (rather than the template text, say).
Recursive includes are illegal.
The template files included are only scanned once, when they are first encountered during expansion. Hence any changes made to an included template by any cunning but clearly pathological template application will not be recognised.
comments - a comment may be included thus -
[comment:<text>]
The comment text must conform to normal rules of template syntax, but is otherwise ignored. That is, the text is scanned for form but is otherwise discarded prior to expansion or evaluation.
Note that all constructs which take a <text> argument may
contain other constructs as part of the text. Eg
To set a default value:
[ifndef(limit):using default limit value[set(limit):40]]
To output some text when a clump is empty:
[set(playerText):[clump()players]$player$[ifndef(last):, ]]
[ifdef(playerText):Players this week are $playerText$]
[ifndef(playerText):No one wants to play!]
Constructs and text arguments may stretch across an arbitrary number of lines, as long as the closing square bracket is present.
Template is an OO module which has the following methods which are of use to the caller:
new - class level method taking a hash of clump name to clump
handler and returning a Template object.
expandFile - taking a filename and a hash of token name to token
value. The return value is the expanded content of the file.
expandLines - taking an array of strings and a hash of token name to token
value. The return value is the expanded content of the passed lines.
A typical code fragment using the Template module might be as follows
(where gameClump and moveClump are both clump handlers):
$gameFile = shift;
$templateFile = shift;
%tokens = ("game" => $gameFile);
%clump_handlers =
( "game" => \&gameClump,
"move" => \&moveClump
);
$tplexp = Template->new(\%clump_handlers);
$output = $tplexp->expandFile($templateFile, \%tokens);
Important points to note in this sample are:
newConstructor for Template. Takes an optional single argument of a reference to a hash of clump name to clump handler function reference, and returns a suitably blessed object reference.
Clump handlers themselves must behave as follows:
Token contexts are inherited - that is, any token which cannot be resolved in
the active context will be looked for in the invoking context, and so on up.
The exceptions to this propagation rule are the local only tokens first and
last.
v0.3: the old style of clump handler returning an actual array of token hash references rather than a reference is still supported, but is deprecated. Recognising empty clumps is much easier with the new interface.
The details of how a clump populates the row data are not specifiable here, and are highly individual to the application. As an example of a simple clump handler, here is a possible definition of a clump handler for daysOfWeek mentioned earlier:
sub daysOfWeekClump()
{
my ($tokens, $count) = (shift, shift);
my @rows = ();
@daysOfWeek = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
$i = 0;
foreach $c (@daysOfWeek)
{
# abort if loop limit exceeded (unlikely in this case, but hey...)
if ($count ne "" && $i >= $count)
{
return @rows;
}
# need to declare this local to ensure that we get a fresh hash
# object for each row.
my %h = ("day" => $c);
push @rows, \%h;
$i++;
}
return @rows;
}
If row data can be precomputed, it may be easier to not use clump handlers and to insert row data directly into the token context passed into the appropriate expand function.
Template::ClumpIteratorInterface for clump iterator which may be substituted for a fully defined clump set in a token hash. This allows iteration across data sets which are either not yet fully defined, or which are not reasonable to hold entirely in memory.
Note that the usual properties of clumps should be observed (eg first and last tokens should be set, the row number should be defined), and that there is support in this interface for those properties, but it is the responsibility of the implementing iterator to provide the actual implementation.
Methods with default implementations are:
nextnextRow to retrieve the actual row data, then lastRow to determine
if that was the last of it.
Methods which must be defined are:
nextRowlastRow
expandFileFunction to expand the text in the given file. Takes arguments of the filename to read from, and the token context within which the file's text is to be expanded (a reference to a token hash). An optional final argument is a file handle where the expanded text is to be written; if this is supplied then the text will be output as it is expanded.
Returns a result of the expanded text (undef if the file handle argument was supplied). =cut
sub expandFile { my $self = shift; my $filename = shift; my $tokens = shift(); my $fh = shift; my $text = $self->readFile($filename); my $result = $self->expandLines($text, $tokens, $fh); pop @{$self->{FILE_STACK}}; return $result; }
expandLinesFunction to expand the text in the passed lines. Takes arguments of a reference to an array of strings to expand, and the token context within which the text is to be expanded (a reference to a token hash). An optional final argument is a file handle where the expanded text is to be written; if this is supplied then the text will be output as it is expanded.
Returns a result of the expanded text (undef if file handle supplied).
parseFileFunction to parse the text in the given file. Takes argument of the filename to read from.
Returns a result of the parse tree. The file path relative to the initial template file location is on the top of the file stack.
parseLinesFunction to parse the text in the passed lines. Takes arguments of a reference to an array of strings to parse.
Returns a result of the parse tree.