package Template::Simple; use warnings; use strict; use Carp ; use File::Slurp ; use Data::Dumper ; our $VERSION = '0.01'; my %opt_defaults = ( pre_delim => qr/\[%/, post_delim => qr/%\]/, greedy_chunk => 0, # upper_case => 0, # lower_case => 0, include_paths => [ qw( templates ) ], ) ; sub new { my( $class, %opts ) = @_ ; my $self = bless {}, $class ; # get all the options or defaults into the object while( my( $name, $default ) = each %opt_defaults ) { $self->{$name} = defined( $opts{$name} ) ? $opts{$name} : $default ; } # make up the regexes to parse the markup from templates # this matches scalar markups and grabs the name $self->{scalar_re} = qr{ $self->{pre_delim} \s* # optional leading whitespace (\w+?) # grab scalar name \s* # optional trailing whitespace $self->{post_delim} }xi ; # case insensitive #print "RE <$self->{scalar_re}>\n" ; # this grabs the body of a chunk in either greedy or non-greedy modes my $chunk_body = $self->{greedy_chunk} ? qr/.+/s : qr/.+?/s ; # this matches a marked chunk and grabs its name and text body $self->{chunk_re} = qr{ $self->{pre_delim} \s* # optional leading whitespace START # required START token \s+ # required whitespace (\w+?) # grab the chunk name \s* # optional trailing whitespace $self->{post_delim} ($chunk_body) # grab the chunk body $self->{pre_delim} \s* # optional leading whitespace END # required END token \s+ # required whitespace \1 # match the grabbed chunk name \s* # optional trailing whitespace $self->{post_delim} }xi ; # case insensitive #print "RE <$self->{chunk_re}>\n" ; # this matches a include markup and grabs its template name $self->{include_re} = qr{ $self->{pre_delim} \s* # optional leading whitespace INCLUDE # required INCLUDE token \s+ # required whitespace (\w+?) # grab the included template name \s* # optional trailing whitespace $self->{post_delim} }xi ; # case insensitive # load in any templates $self->add_templates( $opts{templates} ) ; return $self ; } sub expand { my( $self, $template, $data ) = @_ ; # make a copy if a scalar ref is passed as the template text is # modified in place my $tmpl_ref = ref $template eq 'SCALAR' ? $template : \$template ; my $expanded = $self->_expand_includes( $tmpl_ref ) ; #print "INC EXP <$expanded>\n" ; $expanded = eval { $self->_expand_chunk( $expanded, $data ) ; } ; croak "Template::Simple $@" if $@ ; return $expanded ; } sub _expand_includes { my( $self, $tmpl_ref ) = @_ ; # make a copy of the initial template so we can expand it. my $expanded = ${$tmpl_ref} ; # loop until we can expand no more include markups 1 while $expanded =~ s{$self->{include_re}} { ${ $self->_get_template($1) } }e ; return \$expanded ; } my %expanders = ( HASH => \&_expand_hash, ARRAY => \&_expand_array, CODE => \&_expand_code, # if no ref then data is a scalar so replace the template with just the data '' => sub { \$_[2] }, ) ; sub _expand_chunk { my( $self, $tmpl_ref, $data ) = @_ ; #print "T ref [$tmpl_ref] [$$tmpl_ref]\n" ; #print "CHUNK TMPL\n<$$tmpl_ref>\n" ; #print Dumper $data ; return \'' unless defined $data ; # now expand this chunk based on the type of data my $expander = $expanders{ref $data} ; #print "EXP $expander\nREF ", ref $data, "\n" ; die "unknown template data type '$data'\n" unless defined $expander ; return $self->$expander( $tmpl_ref, $data ) ; } sub _expand_hash { my( $self, $tmpl_ref, $href ) = @_ ; return $tmpl_ref unless keys %{$href} ; # print "T ref [$tmpl_ref] [$$tmpl_ref]\n" ; # print "HASH TMPL\n$$tmpl_ref\n" ; # we need a local copy of the template to expand my $expanded = ${$tmpl_ref} ; # recursively expand all top level chunks in this chunk $expanded =~ s{$self->{chunk_re}} { # print "CHUNK $1\nBODY\n----\n<$2>\n\n------\n" ; ${$self->_expand_chunk( \$2, $href->{$1} ) }}gex ; # now expand scalars #print "HASH TMPL\n<$expanded>\n" ; #print Dumper $href ; $expanded =~ s{$self->{scalar_re}} { #print "SCALAR $1 VAL $href->{$1}\n" ; defined $href->{$1} ? $href->{$1} : '' }ge ; #print "HASH2 TMPL\n$$expanded\n" ; return \$expanded ; } sub _expand_array { my( $self, $tmpl_ref, $aref ) = @_ ; # expand this $tmpl_ref for each element of the aref and join them my $expanded ; #print Dumper $aref ; $expanded .= ${$self->_expand_chunk( $tmpl_ref, $_ )} for @{$aref} ; return \$expanded ; } sub _expand_code { my( $self, $tmpl_ref, $cref ) = @_ ; my $expanded = $cref->( $tmpl_ref ) ; croak <{templates}}{ keys %{$tmpls} } = map ref $_ eq 'SCALAR' ? \"${$_}" : \"$_", values %{$tmpls} ; #print Dumper $self->{templates} ; return ; } sub delete_templates { my( $self, @names ) = @_ ; @names = keys %{$self->{templates}} unless @names ; delete @{$self->{templates}}{ @names } ; delete @{$self->{template_paths}}{ @names } ; return ; } sub _get_template { my( $self, $tmpl_name ) = @_ ; #print "INC $tmpl_name\n" ; my $tmpls = $self->{templates} ; # get the template from the cache and send it back if it was found there my $template = $tmpls->{ $tmpl_name } ; return $template if $template ; # not found, so find, slurp in and cache the template $template = $self->_find_template( $tmpl_name ) ; $tmpls->{ $tmpl_name } = $template ; return $template ; } sub _find_template { my( $self, $tmpl_name ) = @_ ; foreach my $dir ( @{$self->{include_paths}} ) { my $tmpl_path = "$dir/$tmpl_name.tmpl" ; print "PATH: $tmpl_path\n" ; next unless -r $tmpl_path ; # cache the path to this template $self->{template_paths}{$tmpl_name} = $tmpl_path ; # slurp in the template file and return it as a scalar ref return scalar read_file( $tmpl_path, scalar_ref => 1 ) ; } croak <{include_paths}}' CROAK } 1; # End of Template::Simple __END__ =head1 NAME Template::Simple - A simple and fast template module =head1 VERSION Version 0.01 =head1 SYNOPSIS use Template::Simple; my $tmpl = Template::Simple->new(); my $template = < { date => 'Jan 1, 2008', author => 'Me, myself and I', }, row => [ { first => 'row 1 value 1', second => 'row 1 value 2', }, { first => 'row 2 value 1', second => 'row 2 value 2', }, ], footer_data => { modified => 'Aug 31, 2006', }, } ; my $expanded = $tmpl->expand( $template, $data ) ; =head1 DESCRIPTION Template::Simple has these goals: =over 4 =item * Support most common template operations It can recursively include other templates, replace tokens (scalars), recursively expand nested chunks of text and expand lists. By using simple idioms you can get conditional expansions. =item * Complete isolation of template from program code This is very important as template design can be done by different people than the program logic. It is rare that one person is well skilled in both template design and also programming. =item * Very simple template markup (only 4 markups) The only markups are C, C, C and C. See MARKUP for more. =item * Easy to follow expansion rules Expansion of templates and chunks is driven from a data tree. The type of the data element used in an expansion controls how the expansion happens. The data element can be a scalar or scalar reference or an array, hash or code reference. =item * Efficient template expansion Expansion is very simple and uses Perl's regular expressions efficiently. Because the markup is so simple less processing is needed than many other templaters. Precompiling templates is not supported yet but that optimization is on the TODO list. =item * Easy user extensions User code can be called during an expansion so you can do custom expansions and plugins. Closures can be used so the code can have its own private data for use in expanding its template chunk. =back =head2 new() You create a Template::Simple by calling the class method new: my $tmpl = Template::Simple->new() ; All the arguments to C are key/value options that change how the object will do expansions. =over 4 =item pre_delim This option sets the string or regex that is the starting delimiter for all markups. You can use a plain string or a qr// but you need to escape (with \Q or \) any regex metachars if you want them to be plain chars. The default is qr/\[%/. my $tmpl = Template::Simple->new( pre_delim => '<%', ); my $expanded = $tmpl->expand( '<%FOO%]', 'bar' ) ; =item post_delim This option sets the string or regex that is the ending delimiter for all markups. You can use a plain string or a qr// but you need to escape (with \Q or \) any regex metachars if you want them to be plain chars. The default is qr/%]/. my $tmpl = Template::Simple->new( post_delim => '%>', ); my $expanded = $tmpl->expand( '[%FOO%>', 'bar' ) ; =item greedy_chunk This boolean option will cause the regex that grabs a chunk of text between the C markups to become greedy (.+). The default is a not-greedy grab of the chunk text. (UNTESTED) =item templates This option lets you load templates directly into the cache of the Template::Simple object. This cache will be searched by the C markup which will be replaced by the template if found. The option value is a hash reference which has template names (the name in the C markup) for keys and their template text as their values. You can delete or clear templates from the object cache with the C method. my $tmpl = Template::Simple->new( templates => { foo => < <expand( $template, { baz => 'blue', quux => 'color, } ) ; =item include_paths Template::Simple can also load C templates from files. This option lets you set the directory paths to search for those files. Note that the template name in the C markup has the .tmpl suffix appended to it when searched for in one of these paths. The loaded file is cached inside the Template::Simple object along with any loaded by the C option. =back =head1 METHODS =head2 expand( $template, $data ) =head2 add_templates This method adds templates to the object cache. It takes a list of template names and texts just like the C constructor option. $tmpl->add_templates( { foo => \$foo_template, bar => '[%include bar%]', } ) ; =head2 delete_templates This method takes a list of template names and will delete them from the template cache in the object. If you pass in an empty list then all the templates will be deleted. This can be used when you know a template file has been updated and you want to get it loaded back into the cache. Note that you can delete templates that were loaded directly (via the C constructor option or the C method) or loaded from a file. # this deletes only the foo and bar templates from the object cache $tmpl->delete_templates( qw( foo bar ) ; # this deletes all of templates from the object cache $tmpl->delete_templates() ; =head2 get_dependencies This method expand the only C markups of a template and it returns a list of the file paths that were found and loaded. It is meant to be used to build up a dependency list of included templates for a main template. Typically this can be called from a script (see TODO) that will do this for a set of main templates and will generate Makefile dependencies for them. Then you can regenerate expanded templates only when any of their included templates have changed. It takes a single argument of a template. UNKNOWN: will this require a clearing of the cache or will it do the right thing on its own? or will it use the file path cache? my @dependencies = $tmpl->get_dependencies( '[%INCLUDE top_level%]' ); =head1 MARKUP All the markups in Template::Simple use the same delimiters which are C<[%> and C<%]>. You can change the delimiters with the C and C options in the C constructor. =head2 Tokens A token is a single markup with a C<\w+> Perl word inside. The token can have optional whitespace before and after it. A token is replaced by a value looked up in a hash with the token as the key. The hash lookup keeps the same case as parsed from the token markup. [% foo %] [%BAR%] Those will be replaced by C<$href->{foo}> and C<$href->{BAR}> assuming C<$href> is the current data for this expansion. Tokens are only parsed out during hash data expansion so see Hash Data for more. =head2 Chunks Chunks are regions of text in a template that are marked off with a start and end markers with the same name. A chunk start marker is C<[%START name%]> and the end marker for that chunk is C<[%END name%]>. C is a C<\w+> Perl word which is the name of this chunk. The whitespace between C and C is required and there is optional whitespace before C and after the C. C are case insensitive but the C's case is kept. C must match in the C pair and it used as a key in a hash data expansion. Chunks are the primary way to markup templates for structures (sets of tokens), nesting (hashes of hashes), repeats (array references) and callbacks to user code. Chunks are only parsed out during hash data expansion so see Hash Data for more. The body of text between the C markups is grabbed with a C<.+?> regular expression with the /s option enabled so it will match all characters. By default it will be a non-greedy grab but you can change that in the constructor by enabling the C option. [%Start FOO%] [% START bar %] [% field %] [% end bar %] [%End FOO%] =head2 Includes =head1 EXPANSION RULES Template::Simple has a short list of expansion rules and they are easy to understand. There are two types of expansions, include expansion and chunk expansion. In the C method, the template is an unnamed top level chunk of text and it first gets its C markups expanded. The text then undergoes a chunk expansion and a scalar reference to that expanded template is returned to the caller. =head2 Include Expansion Include expansion is performed one time on a top level template. When it is done the template is ready for chunk expansion. Any markup of the form C<[%INCLUDE name]%> will be replaced by the text found in the template C. The template name is looked up in the object's template cache and if it is found there its text is used as the replacement. If a template is not found in the cache, it will be searched for in the list of directories in the C option. The file name will be a directory in that list appended with the template name and the C<.tmpl> suffix. The first template file found will be read in and stored in the cache. Its path is also saved and those will be returned in the C method. See the C and C methods and the C option. Expanded include text can contain more C markups and they will also be expanded. The include expansion phase ends where there are no more C found. =head2 Chunk Expansion A chunk is the text found between C and C markups and it gets its named from the C markup. The top level template is considered an unamed chunk and also gets chunk expanded. The data for a chunk determines how it will be expanded. The data can be a scalar or scalar reference or an array, hash or code reference. Since chunks can contain nested chunks, expansion will recurse down the data tree as it expands the chunks. Each of these expansions are explained below. Also see the IDIOMS and BEST PRACTICES section for examples and used of these expansions. =head2 Scalar Data Expansion If the current data for a chunk is a scalar or scalar reference, the chunk's text in the templated is replaced by the scalar's value. This can be used to overwrite one default section of text with from the data tree. =head2 Code Data Expansion If the current data for a chunk is a code reference (also called anonymous sub) then the code reference is called and it is passed a scalar reference to the that chunk's text. The code must return a scalar or a scalar reference and its value replaces the chunk's text in the template. If the code returns any other type of data it is a fatal error. Code expansion is how you can do custom expansions and plugins. A key idiom is to use closures as the data in code expansions and keep the required outside data in the closure. =head2 Array Data Expansion If the current data for a chunk is an array reference do a full chunk expansion for each value in the array. It will replace the original chunk text with the joined list of expanded chunks. This is how you do repeated sections in Template::Simple and why there is no need for any loop markups. Note that this means that expanding a chunk with $data and [ $data ] will do the exact same thing. A value of an empty array C<[]> will cause the chunk to be replaced by the empty string. =head2 Hash Data Expansion If the current data for a chunk is a hash reference then two phases of expansion happen, nested chunk expansion and token expansion. First nested chunks are parsed of of this chunk along with their names. Each parsed out chunk is expanded based on the value in the current hash with the nested chunk's name as the key. If a value is not found (undefined), then the nested chunk is replaced by the empty string. Otherwise the nested chunk is expanded according to the type of its data (see chunk expansion) and it is replaced by the expanded text. Chunk name and token lookup in the hash data is case sensitive (see the TODO for cased lookups). Note that to keep a plain text chunk or to just have the all of its markups (chunks and tokens) be deleted just pass in an empty hash reference C<{}> as the data for the chunk. It will be expanded but all markups will be replaced by the empty string. =head2 Token Expansion The second phase is token expansion. Markups of the form [%token%] are replaced by the value of the hash element with the token as the key. If a token's value is not defined it is replaced by the empty string. This means if a token key is missing in the hash or its value is undefined or its value is the empty string, the [%token%] markup will be deleted in the expansion. =head1 IDIOMS and BEST PRACTICES With all template systems there are better ways to do things and Template::Simple is no different. This section will show some ways to handle typical template needs while using only the 4 markups in this module. =head2 Conditionals This conditional idiom can be when building a fresh data tree or modifying an existing one. $href->{$chunk_name} = $keep_chunk ? {} : '' ; If you are building a fresh data tree you can use this idiom to do a conditional chunk: $href->{$chunk_name} = {} if $keep_chunk ; To handle an if/else conditional use two chunks, with the else chunk's name prefixed with NOT_ (or use any name munging you want). Then you set the data for either the true chunk (just the plain name) or the false trunk with the NOT_ name. You can use a different name for the else chunk if you want but keeping the names of the if/else chunks related is a good idea. Here are two ways to set the if/else data. The first one uses the same data for both the if and else chunks and the second one uses different data so the it uses the full if/else code for that. $href->{ ($boolean ? '' : 'NOT_') . $chunk_name} = $data if ( $boolean ) { $href->{ $chunk_name} = $true_data ; else { $href->{ "NOT_$chunk_name" } = $false_data ; } NOTE TO ALPHA USERS: i am also thinking that a non-existing key or undefined hash value should leave the chunk as is. then you would need to explicitly replace a chunk with the empty string if you wanted it deleted. It does affect the list of styles idiom. Any thoughts on this change of behavior? Since this hasn't been released it is the time to decide this. =head2 Chunked Includes One of the benefits of using include templates is the ability to share and reuse existing work. But if an included template has a top level named chunk, then that name would also be the same everywhere where this template is included. If a template included another template in multiple places, its data tree would use the same name for each and not allow unique data to be expanded for each include. A better way is to have the current template wrap an include markup in a named chunk markup. Then the data tree could use unique names for each included template. Here is how it would look: [%START foo_prime%][%INCLUDE foo%][%START foo_prime%] random noise [%START foo_second%][%INCLUDE foo%][%START foo_second%] See the TODO section for some ideas on how to make this even more high level. =head2 Repeated Sections If you looked at the markup of Template::Simple you have noticed that there is no loop or repeat construct. That is because there is no need for one. Any chunk can be expanded in a loop just by having its expansion data be an anonymous array. The expander will loop over each element of the array and do a fresh expansion of the chunk with this data. A join (on '') of the list of expansions replaces the original chunk and you have a repeated chunk. =head2 A List of Mixed Styles One formating style is to have a list of sections each which can have its own style or content. Template::Simple can do this very easily with just a 2 level nested chunk and an array of data for expansion. The outer chunk includes (or contains) each of the desired styles in any order. It looks like this: [%START para_styles%] [%START main_style%] [%INCLUDE para_style_main%] [%END main_style%] [%START sub_style%] [%INCLUDE para_style_sub%] [%END sub_style%] [%START footer_style%] [%INCLUDE para_style_footer%] [%END footer_style%] [%END para_styles%] The other part to make this work is in the data tree. The data for para_styles should be a list of hashes. Each hash contains the data for one pargraph style which is keyed by the style's chunk name. Since the other styles's chunk names are not hash they are deleted. Only the style which has its name as a key in the hash is expanded. The data tree would look something like this: [ { main_style => $main_data, }, { sub_style => $sub_data, }, { sub_style => $other_sub_data, }, { footer_style => $footer_data, }, ] =head1 TESTS The test scripts use a common test driver module in t/common.pl. It is passed a list of hashes, each of which has the data for one test. A test can create a ne Template::Simple object or use the one from the previous test. The template source, the data tree and the expected results are also important keys. See the test scripts for examples of how to write tests using this common driver. =over 4 =item name This is the name of the test and is used by Test::More =item opts This is a hash ref of the options passed to the Template::Simple constructor. The object is not built if the C key is set. =item keep_obj If set, this will make this test keep the Template::Simple object from the previous test and not build a new one. =item template This is the template to expand for this test. =item data This is the data tree for the expansion of the template. =item expected This is the text that is expected after the expansion. =item skip If set, this test is skipped. =back =head1 TODO Even though this template system is simple, that doesn't mean it can't be extended in many ways. Here are some features and designs that would be good extensions which add useful functionality without adding too much complexity. =head2 Compiled Templates A commonly performed optimization in template modules is to precompile (really preparse) templates into a internal form that will expand faster. Precompiling is slower than expansion from the original template which means you won't want to do it for each expansion. This means it has a downside that you lose out when you want to expand using templates which change often. Template::Simple makes it very easy to precompile as it already has the regexes to parse out the markup. So instead of calling subs to do actual expansion, a precompiler would call subs to generate a compiled expansion tree. The expansion tree can then be run or processes with expansion data passed to it. You can think of a precompiled template as having all the nested chunks be replaced by nested code that does the same expansion. It can still do the dynamic expansion of the data but it saves the time of parsing the template souice. There are three possible internal formats for the precompiled template: =over 4 =item Source code This precompiler will generate source code that can be stored and/or eval'ed. The eval'ed top level sub can then be called and passed the expansion data. =item Closure call tree The internal format can be a nested set of closures. Each closure would contain private data such as fixed text parts of the original template, lists of other closures to run, etc. It is trivial to write a basic closure generator which will make build this tree a simple task. =item Code ref call tree This format is a Perl data tree where the nodes have a code reference and its args (which can be nested instances of the same nodes). Instead of executing this directly, you will need a small interpreter to execute all the code refs as it runs through the tree. This would make for a challenging project to any intermediate Perl hacker. It just involves knowing recursion, data trees and code refs. Contact me if you are interested in doing this. =back =head2 Cased Hash Lookups One possible option is to allow hash expansions to always use upper or lower cased keys in their lookups. =head2 Expand tokens before includes and chunks Currently tokens are expanded after includes and chunks. If tokens were expanded in a pass before the others, the include and chunk names could be dynamically set. This would make it harder to precompile templates as too much would be dynamic, i.e. you won't know what the fixed text to parse out is since anything can be included at expand time. But the extra flexibility of changing the include and chunk names would be interesting. It could be done easily and enabled by an option. =head2 Plugins There are two different potential areas in Template::Simple that could use plugins. The first is with the expansion of chunkas and dispatching based on the data type. This dispatch table can easily be replaced by loaded modules which offer a different way to expand. These include the precompiled expanders mentioned above. The other area is with code references as the data type. By defining a closure (or a closure making) API you can create different code refs for the expansion data. The range of plugins is endless some of the major template modules have noticed. One idea is to make a closure which contains a different Template::Simple object than the current one. This will allow expansion of a nested chunk with different rules than the current chunk being expanded. =head2 Data Escaping Some templaters have options to properly escape data for some types of text files such as html. this can be done with some variant of the _expand_hash routine which also does the scalar expansion (which is where data is expanded). The expanding scalars code could be factored out into a set of subs one of which is used based on any escaping needs. =head2 Data Tree is an Object This is a concept I don't like but it was requested so it goes into the TODO file. Currently C can only be passed a regular (unblessed) ref (or a scalar) for its data tree. Passing in an object would break encapsulation and force the object layout to be a hash tree that matches the layout of the template. I doubt that most objects will want to be organized to match a template. I have two ideas, one is that you add a method to that object that builds up a proper (unblessed) data tree to pass to C. The other is by subclassing C and overriding C with a sub that does take an object hash and it can unbless it or build a proper data tree and then call C in SUPER::. A quick solution is to use C (from Scalar::Utils) instead of C to allow object hashes to be passed in. =head2 Includes and Closure Synergy By pairing up an include template along with code that can generate the appropriate data tree for its expansion, you can create a higher level template framework (the synergy). Additional code can be associated with them that will handle input processing and verification for the templates (e.g. web forms) that need it. A key to this will be making all the closures for the data tree. This can be greatly simplified by using a closure maker sub that can create all the required closures. =head2 Metafields and UI Generation Taking the synergy up to a much higher level is the concept of meta knowledge of fields which can generate templates, output processing (data tree generation), input processing, DB backing and more. If you want to discuss such grandiose wacky application schemes in a long rambling mind bending conversation, please contact me. =head2 More Examples and Idioms As I convert several scripts over to this module (they all used the hack version), I will add them to an examples section or possibly put them in another (pod only) module. Similarly the Idioms section needs expansion and could be also put into a pod module. One goal requested by an early alpha tester is to keep the primary docs as simple as the markup itself. This means moving all the extra stuff (and plenty of that) into other pod modules. All the pod modules would be in the same cpan tarball so you get all the docs and examples when you install this. =head1 AUTHOR Uri Guttman, C<< >> =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc Template::Simple You can also look for information at: =over 4 =item * RT: CPAN's request tracker L =item * Search CPAN L =back =head1 ACKNOWLEDGEMENTS I wish to thank Turbo10 for their support in developing this module. =head1 COPYRIGHT & LICENSE Copyright 2006 Uri Guttman, all rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut find templates and tests deep nesting tests greedy tests methods pod delete_templates test pod cleanup fine edit more tests slurp dependency in makefile.pl