DTL::Fast - Perl implementation of Django templating language.
Version 2017.1
Complie and render template from code:
use DTL::Fast; my $tpl = DTL::Fast::Template->new('Hello, {{ username }}!'); print $tpl->render({ username => 'Alex'});
Or create a file: template.txt in /home/alex/templates with contents:
Hello, {{ username }}!
And load and render it:
use DTL::Fast qw( get_template ); my $tpl = get_template( 'template.txt', dirs => ['/home/alex/templates'] ); print $tpl->render({ username => 'Alex'});
This module is a Perl and stand-alone templating system, cloned from Django templating sytem, described in here.
Goals of this implementation are:
Speed in mod_perl/FCGI environment
Possibility to cache using files/memcached
Maximum compatibility with original Django templates
Current release implements almost all tags and filters documented on Django site. Also, some extensions has been made to the tags, filters and operators.
Some optimisation has been done and some critical section been implemeted in C.
Internationalization and localization are not yet implemented.
You may get template object using three ways.
Using DTL::Fast::Template constructor:
use DTL::Fast; my $tpl = DTL::Fast::Template->new( $template_text, # template itself 'dirs' => [ $dir1, $dir2, ... ], # optional, directories list to look for parent templates and includes 'ssi_dirs' => [ $ssi_dir1, $ssi_dir1, ...] # optional, directories list allowed to used in ssi tag (ALLOWED_INCLUDE_ROOTS in Django) 'url_source' => \&uri_getter # optional, reference to a function, that can return url template by model name (necessary for url tag) );
use DTL::Fast qw(get_template); my $tpl = get_template( $template_path, # path to the template, relative to directories from second argument 'dirs' => [ $dir1, $dir2, ... ], # mandatory, directories list to look for parent templates and includes 'ssi_dirs' => [ $ssi_dir1, $ssi_dir1, ...] # optional, directories list allowed to used in ssi tag (ALLOWED_INCLUDE_ROOTS in Django) 'url_source' => \&uri_getter # optional, reference to a function, that can return url template by model name (necessary for url tag) );
when you are using get_template helper function, framework will try to find template in following files: $dir1/$template_path, $dir2/$template_path ... Searching stops on first occurance.
get_template
$dir1/$template_path, $dir2/$template_path ...
use DTL::Fast qw(select_template); my $tpl = select_template( [ $template_path1, $template_path2, ...], # paths to templates, relative to directories from second argument 'dirs' => [ $dir1, $dir2, ... ], # mandatory, directories list to look for parent templates and includes 'ssi_dirs' => [ $ssi_dir1, $ssi_dir1, ...] # optional, directories list allowed to used in ssi tag (ALLOWED_INCLUDE_ROOTS in Django) 'url_source' => \&uri_getter # optional, reference to a function, that can return url template by model name (necessary for url tag) );
when you are using select_template helper function, framework will try to find template in following files: $dir1/$template_path1, $dir1/$template_path2 ... Searching stops on first occurance.
select_template
$dir1/$template_path1, $dir1/$template_path2 ...
After parsing template using one of the methods above, you may render it using context. Context is basically a hash of values, that will be substituted into template. Hash may contains scalars, hashes, arrays, objects and methods. Into render method you may pass a Context object or just a hashref (in which case Context object will be created automatically).
render
use DTL::Fast qw(get_template); my $tpl = get_template( 'hello_template.txt', 'dirs' => [ '/srv/wwww/templates/' ] ); print $tpl->render({ name => 'Alex' }); print $tpl->render({ name => 'Ivan' }); print $tpl->render({ name => 'Sergey' });
or
use DTL::Fast qw(get_template); my $tpl = get_template( 'hello_template.txt', 'dirs' => [ '/srv/wwww/templates/' ] ); my $context = DTL::Fast::Context->new({ 'name' => 'Alex' }); print $tpl->render($context); $context->set('name' => 'Ivan'); print $tpl->render($context); $context->set('name' => 'Sergey'); print $tpl->render($context);
use DTL::Fast qw(register_tag); register_tag( 'mytag' => 'MyTag::Module' );
This method registers or overrides registered tag keyword with handler module. Module will be loaded when first encountered during template parsing. About handler modules you may read in "CUSTOM TAGS" section.
use DTL::Fast qw(preload_tags); preload_tags();
Preloads all registered tags modules. Mostly for debugging purposes or persistent environment stability.
use DTL::Fast qw(register_filter); register_filter( 'myfilter' => 'MyFilter::Module' );
This method registers or overrides registered filter keyword with handler module. Module will be loaded when first encountered during template parsing. About handler modules you may read in "CUSTOM FILTERS" section.
use DTL::Fast qw(preload_filters); preload_filters();
Preloads all registered filters modules. Mostly for debugging purposes or persistent environment stability.
use DTL::Fast qw(register_operator); register_operator( 'xor' => [ 1, 'MyOps::XOR' ], 'myop' => [ 0, 'MyOps::MYOP' ], );
This method registers or overrides registered operator handlers. Handler module will be loaded when first encountered during template parsing.
Arguments hash is:
'operator_keyword' => [ precedence, handler_module ]
Currently there are 9 precedences from 0 to 8, the lower is less prioritised. You may see built-in precedence in the DTL::Fast::Expression::Operator module.
DTL::Fast::Expression::Operator
More about custom operators you may read in "CUSTOM OPERATORS" section.
use DTL::Fast qw(preload_operators); preload_operators();
Preloads all registered operators modules. Mostly for debugging purposes or persistent environment stability.
my $protected_text = DTL::Fast::html_protect($raw_text);
This function protects string for a safe HTML output by replacing characters: < > & " ' with their HTML equivalents. Function written in C and works pretty fast. If you want it to properly treat UTF-8 characters you must set utf8 flag for $raw_text.
$raw_text
my $spaceless_text = DTL::Fast::spaceless($raw_text);
This function implements spaceless tag, by removing spaces, tabs and newlines between < and > symbols. Written in C and works pretty fast.If you want it to properly treat UTF-8 characters you must set utf8 flag for $raw_text.
eval 'some code ... '; my $sequence = DTL::Fast::eval_sequence();
This function returns internal perl eval counter. This counter being increased on every eval $string; operation. Counter may be useful for debugging purposes with multiple evals, in order to detect in which exact eval error occured. Perl's die method says smth like: error in eval(42) .... 42 in this example is eval sequence counter.
eval $string;
die
error in eval(42) ...
Context is basically a hash with data, wrapped into the DTL::Fast::Context class. You may create it yourself using
DTL::Fast::Context
my $context = DTL::Fast::Context->new($hash_ref);
or let DTL::Fast create it for you implicitly, by calling
my $template = DTL::Fast::Template->new('Hi there {{ username }}!'); $template->render($hash_ref);
Where $hash_ref is a reference to your data. If your context is:
$hash_ref
$context = { names => [ 'Alexandr' ] };
And your template is like:
Hello {{ names.0 }}!
Rendering result will be:
Hello Alexandr!
Context has one configuration option you may change via $self-returning setter.
$self
Prior to version 2017.1, context died if you've tried to get non-existing deep field:
my $context = DTL::Fast::Context->new({ somearray => [], somevar => 42 }); ... {{ somevar }} # ok, renders 42 {{ ohtervar }} # ok, renders nothing (undef) {{ somearray.0 }} # ok, renders nothing, (undef) {{ otherray.0 }} # ok, renders nothing, (undef). FYI: this is actually a bug, # if first step is not defined, we are not dying. Don't rely on this, will be fixed. {{ somevar.someval }} # not ok, dying. 42 is not a hash/object reference and we cant' traverce it deeper
Such behaviour was implemented from practical reasons to make development stricter. But, what is good for development may be not so good for production. Was not problem in my case, but it's a good point. Default behavior stays the same, but you have a possibility to change it:
my $context->set_die_on_missing_path(0); # Method is self-returning, so it's pretty fine to use it with chain: $template->render( DTL::Fast::Context->new($hashref)->set_die_on_missing_path(0) ); # Or you may pass this option to a constructor, like: $template->render(DTL::Fast::Context->new($hashref, die_on_missing_path => 0));
Expressions used in if tag consists of variables, operators and brackets. All operators MUST have at least one space symbol before and after it. This is an after-effect of custom parsing algorythm.
if
{% if var1 == var2 %} # correct {% if var1==var2 %} # not correct, no spaces around ==
Module supports following operators (with precedence):
pow, ** # left operand in power of the right one defined # check if right operand is defined not # negating the right operand in, not in # check that left operand exists in a hash or an array (right operand) *, /, %, mod # multiplying, dividing and modulus +, - # plus and minus ==, !=, <>, <, >, <=, >= # comparision operators and # logical and or # logical or
This module supports almost all built-in tags documented on official Django site. Don't forget to read incompatibilities and extensions sections.
New tag for using with inheritance in order to render a parent block. In Django you are using {{ block.super }} which is also currently supported, but depricated and will be removed in future versions.
{{ block.super }}
New tag, that works like firstof tag, but checks if value is defined (not true)
firstof
{% sprintf pattern var1 var2 ... varn %}
Works exactly like a perl's sprintf function with pattern and substitutions. This tag was recently implemented and should be considered as experimental.
url tag works a different way. Because there is no framework around, we can't obtain model's path the same way. But you may pass url_source parameter into template constructor or get_template/select_template function. This parameter MUST be a reference to a function, that will return to templating engine url template by some 'model path' (first parameter of url tag). Second parameter passed to the url_source handler will be a reference to array of argument values (in case of positional arguments) or reference to a hash of arguments (in case of named ones). Url source handler may just return a regexp template by model path and templating engine will try to restore it with specified arguments. Or, you may restore it yourself, alter replacement arguments or do whatever you want.
url
url_source
{% dump var1 var2 ... varn %}
dump tag is useful for development and debugging. Dumps context variables using Data::Dumper to the rendered template as is.
dump
Data::Dumper
{% dump_html var1 var2 ... varn %}
Works exactly as the dump tag, but escapes result and writes it to the rendered template, wrapped with textarea tags. Textarea has dtl_fast_dump_area class selector, so you may do any styling on your side. Useful for debugging HTML pages.
textarea
dtl_fast_dump_area
{% dump_warn var1 var2 ... varn %}
Works exactly as the dump tag, warns output, instead of putting it into the rendered template.
This module supports all built-in filters documented on official Django site. Don't forget to read incompatibilities and extensions sections.
{{ var1|numberformat }}
Formats 12345678.9012 as
12 345 678.9012
Split integer part of the number by 3 digits, separated by spaces.
Reverses data depending on type:
Scalar will be reversed literally: "hi there" => "ereht ih"
Array will be reversed using perl's reverse function
Hash will be reversed using perl's reverse function
Object may provide reverse method to be used with this filter
{{ var1|split:"\s+"|slice:":2"|join:"," }}
Splitting variable with specified pattern, using Perl's split function. Current implementation uses //s regexp. Filter returns array. This filter was recently implemented and should be considered as experimental.
Formatting timestamp using Date::Format module. This is C-style date formatting, not PHP one.
Date::Format
To do...
You may extend DTL::Fast with your own custom tags. It's pretty simple process. There are two types of tags in the library: simple tags and block tags. Simple tag just doing something, like including file and block tags contains other blocks and affects their rendering in some way, like for loop.
DTL::Fast
Every tag is a separate class inherited from certain prototype.
Every simple tag implemented as class, inherited from DTL::Fast::Tag::Simple:
DTL::Fast::Tag::Simple
package CustomTags::MyTag; # your tag class use strict; use utf8; use warnings FATAL => 'all'; # make a clean code use parent 'DTL::Fast::Tag::Simple'; # parent class $DTL::Fast::TAG_HANDLERS{'my_tag'} = __PACKAGE__; # register your tag keyword with your package in tag handlers # You may need to override a constructor in case you need to support some complex syntax # if you don't need to do something special, just don't override it # invoked once on parse phase sub new { my( $proto, # well, this is a class proto $parameter, # this is a text with everything after your tag: {% my_tag ...this is a parameter if any... %} %kwargs # additional named arguments may be passed to the constructor ) = @_; .... my $self = $proto->SUPER::new($parameter, %kwargs); .... return $self; } # Parameter parsing method, invoked once on parsing phase. Parent constructor stores passed $parameter # into the $self->{'parameter'} hash entry and invokes parse_parameter method. # here you may make some preparations for rendering, parse arguments and do whatever you want. sub parse_parameters { my $self = shift; # reference to the tag object .... return $self; # IMPORTANT, method must return $self, because it's being used in chain call } # Rendering method. Invoked on every rendering iteration # The main work is being done here sub render { my( $self, # object reference $context, # context object $global_safe # this flag is set to true, if somehow global safety is turned on, means disabled HTML escaping ) = @_; ... return $result; # this is just a string generated by tag. If your tag is silent, just return an empty string }
After creating your tag class, you need to register it in DTL::Fast, so library knew which module to load on tag keyword:
use DTL::Fast qw(register_tag); register_tag( 'my_tag' => 'CustomTags::MyTag' );
Basically, that's all. For more information you may view through the sources of buit-in simple tags.
Every block tag in the library implemented as class, inherited from DTL::Fast::Tag:
DTL::Fast::Tag
package CustomTags::MyBlockTag; # your tag class use strict; use utf8; use warnings FATAL => 'all'; # make a clean code use parent 'DTL::Fast::Tag'; # parent class is Tag, not Tag::Simple $DTL::Fast::TAG_HANDLERS{'my_block_tag'} = __PACKAGE__; # register your tag opening keyword with your package in tag handlers # constructor and parameters parser are the same as in simple tags # method must be defined and must return block close tag, so parser would know where block is ended sub get_close_tag{ return 'end_my_tag';} # optional. Method invoked on parsing phase with next parsed chunk of current block tag contents # this method being used, for example, in the 'if' tag to push chunks into sub-branches, not if block itself sub add_chunk { my( $self, # reference to the tag object $chunk # parsed chunk object, if chunk must be ignored, it would be undefined ) = @_; ... here you may alter chunk or put it in some other place, like subblocks... return $self; # don't forget to return self } # optional. Method invoked on parsing phase for every {% ... .%} construction. # if there is no special controlling keywords, don't override this method sub parse_tag_chunk { my( $self, # reference to the tag object $tag_name, # tag keyword $tag_param, # everything after tag keyword (constructor's $parameter) $chunk_lines # chunk size in lines, used for proper debugging output ) = @_; my $result; # here you may interpret additional tag keywords, like else, elsif and so on. if( $tag_name eq 'my_tag_else' ) { ... do smth special $DTL::Fast::Template::CURRENT_TEMPLATE_LINE += $chunk_lines; # set proper line number for next source block } elsif( $tag_name eq 'my_tag_alternative_end' ) { $self->{'raw_chunks'} = []; # this construction ends current block parsing $DTL::Fast::Template::CURRENT_TEMPLATE_LINE += $chunk_lines; # set proper line number for next source block } else # if it was not special keyword, just do regular work { $result = $self->SUPER::parse_tag_chunk($tag_name, $tag_param, $chunk_lines); } return $result; # result must be a generated chunk or undef if there is no one } # Rendering method. Invoked on every rendering iteration # The main work is being done here sub render { my( $self, # object reference $context, # context object $global_safe # this flag is set to true, if somehow global safety is turned on, means disabled HTML escaping ) = @_; ... my $result = $self->SUPER::render($context, $global_safe); # in order to render current block contents, invoke the parent renderer ... return $result; # this is just a string generated by tag. If your tag is silent, just return an empty string }
use DTL::Fast qw(register_tag); register_tag( 'my_block_tag' => 'CustomTags::MyBlockTag' );
Basically, that's all. For more information you may view through the sources of buit-in block tags.
Every filter in the library implemented as a class, inherited from DTL::Fast::Filter:
DTL::Fast::Filter
package CustomFilters::MyFilter; # class name use strict; use utf8; use warnings FATAL => 'all'; # make your code clean use parent 'DTL::Fast::Filter'; # parent class $DTL::Fast::FILTER_HANDLERS{'myfilter'} = __PACKAGE__; # register your module as a filter handler # optional parameter parser, invoked on parsing phase. # If your filter is parametrised, you may need to override this method to parse parameters, stored in # the $self->{'parameter'} which is an array of DTL::Fast::Variable objects, one for each parameter sub parse_parameters { my( $self ) = @_; ... here you may process your input parameters... return $self; # important to return $self } # main method sub filter { my( $self, # filter object reference $filter_manager, # filter manager object reference $value, # filtering value $context # current context for parametrised filters ) = @_; # here you may modify $value. Don't forget to make a shallow copies on arrays and hashes .... return $value; }
After creating your filter class, you need to register it in DTL::Fast, so library knew which module to load on filter keyword:
use DTL::Fast qw(register_filter); register_filter( 'myfilter' => 'CustomFilters::MyFilter' );
Basically, that's all. For more information you may view through the sources of buit-in filters.
hashes being iterated differently from Python's. You can iterate hash only as key, val in hash. No hash.items, hash.keys, hash.values are supported.
key, val in hash
hash.items
hash.keys
hash.values
{{ block.super }} construction is currently supported, but depricated in favor of {% block_super %} tag.
{% block_super %}
Django's setting ALLOWED_INCLUDE_ROOTS should be passed to tempalte constructor/getter as ssi_dirs argument.
ALLOWED_INCLUDE_ROOTS
ssi_dirs
csrf_token tag is not implemented, too well connected with Django.
csrf_token
_dtl_* variable names in context are reserved for internal system purposes. Don't use them.
_dtl_*
output from following tags: cycle, firstof, firstofdefined are being escaped by default (like in later versions of Django)
cycle
firstofdefined
escapejs filter works other way. It's not translating every non-ASCII character to the codepoint, but just escaping single and double quotes and \n \r \t \0. Utf-8 symbols are pretty valid for javascript/json.
escapejs
\n \r \t \0
fix_ampersands filter is not implemented, because it's marked as depricated and will beremoved in Django 1.8
fix_ampersands
pprint filter is not implemented.
pprint
iriencode filter works like urlencode for the moment.
iriencode
urlencode
urlize filter takes well-formatted url and makes link with this url and text generated by urldecoding and than escaping url link.
urlize
wherever filter in Django returns True/False values, DTL::Fast returns 1/0.
True/False
1/0
May be some of this features implemented in Django itself. Let me know about it.
filters may accept several arguments, and context variables can be used in them, like {{ var|filter1:var2:var3:...:varn }}
defined logical operator. In logical constructions you may use defined operator, which works exactly like perl's defined
defined
alternatively, in logical expresisons you may compare (==,!=) value to undef or None which are synonims
undef
None
slice filter works with ARRAYs, HASHes and SCALARs (or SCALARrefs):
slice
Arrays slicing supports Python's indexing rules and Perl's indexing rules (but Perl's one has no possibility to index from the end of the list).
Scalars slicing works as substring from_index ... to_index. Supports both Perl's and Python's indexes.
Hash slicing options should be a comma-separated keys.
You may use brackets in logical expressions to override natural precedence
forloop context hash inside a for block tag contains additional fields: odd, odd0, even and even0
forloop
for
odd
odd0
even
even0
variables rendering: if any code reference encountered due variable traversing, is being invoked with context argument. Like:
{{ var1.key1.0.func.var2 }}
is being rendered like:
$context->{'var1'}->{'key1'}->[0]->func($context)->{'var2'}
you may use filters with static variables. Like:
{{ "text > test"|safe }}
objects behaviour methods. You may extend your objects, stored in context to make them work properly with some tags and operations:
as_bool - returns logical representation of object
as_bool
and(operand) - makes logical `and` between object and operand
and(operand)
or(operand) - makes logical `or` between object and operand
or(operand)
div(operand) - divides object by operand
div(operand)
equal(operand) - checks if object is equal with operand
equal(operand)
compare(operand) - compares object with operand, returns -1, 0, 1 on less than, equal or greater than respectively
compare(operand)
in(operand) - checks if object is in operand
in(operand)
contains(operand) - checks if object contains operand
contains(operand)
minus(operand) - substitutes operand from object
minus(operand)
plus(operand) - adds operand to object
plus(operand)
mod(operand) - returns reminder from object division to operand
mod(operand)
mul(operand) - multiplicates object by operand
mul(operand)
pow(operand) - returns object powered by operand
pow(operand)
not() - returns object inversion
not()
reverse() - returns reversed object
reverse()
as_array() - returns array representation of object
as_array()
as_hash() - returns hash representation of object
as_hash()
I've compared module speed with previous abandoned implementation: Dotiac::DTL in both modes: FCGI and CGI. Test template and scripts are in /timethese directory. Django templating in Python with cache works about 80% slower than DTL::Fast.
Dotiac::DTL
Template parsing permormance with software cache wiping on each iteration:
Benchmark: timing 5000 iterations of DTL::Fast , Dotiac::DTL... DTL::Fast : 5 wallclock secs ( 4.59 usr + 0.12 sys = 4.71 CPU) @ 1061.36/s (n=5000) Dotiac::DTL: 41 wallclock secs (38.66 usr + 2.22 sys = 40.88 CPU) @ 122.30/s (n=5000)
DTL::Fast parsing templates 8.67 times faster, than Dotiac::DTL.
To run this test, you need to alter Dotiac::DTL module and change declaration of my %cache; to our %cache;.
my %cache;
our %cache;
Rendering of pre-compiled template (software cache):
Benchmark: timing 3000 iterations of DTL::Fast , Dotiac::DTL... DTL::Fast : 34 wallclock secs (33.86 usr + 0.39 sys = 34.25 CPU) @ 87.59/s (n=3000) Dotiac::DTL: 53 wallclock secs (52.93 usr + 0.62 sys = 53.55 CPU) @ 56.02/s (n=3000)
Tests shows, that DTL::Fast works a 56% faster, than Dotiac::DTL in persistent environment.
This test rendered test template many times by external script, invoked via system call:
system
Benchmark: timing 300 iterations of Dotiac render , Fast cached render, Fast render DTL::Fast : 40 wallclock secs ( 0.00 usr 0.12 sys + 35.14 cusr 4.98 csys = 40.24 CPU) @ 7.45/s (n=300) Dotiac::DTL: 51 wallclock secs ( 0.00 usr 0.12 sys + 38.29 cusr 12.63 csys = 51.04 CPU) @ 5.88/s (n=300)
Tests shows, that DTL::Fast works 27% faster, than Dotiac::DTL in CGI environment.
1 Cache key : 0 wallclock secs ( 0.19 usr + 0.00 sys = 0.19 CPU) @ 534759.36/s (n=100000) 2 Decompress : 0 wallclock secs ( 0.27 usr + 0.00 sys = 0.27 CPU) @ 377358.49/s (n=100000) 3 Serialize : 4 wallclock secs ( 3.73 usr + 0.00 sys = 3.73 CPU) @ 26824.03/s (n=100000) 4 Deserialize: 5 wallclock secs ( 4.26 usr + 0.00 sys = 4.26 CPU) @ 23479.69/s (n=100000) 5 Compress : 10 wallclock secs (10.50 usr + 0.00 sys = 10.50 CPU) @ 9524.72/s (n=100000) 6 Validate : 11 wallclock secs ( 3.12 usr + 8.05 sys = 11.17 CPU) @ 8952.55/s (n=100000) 7 Parse : 1 wallclock secs ( 0.44 usr + 0.23 sys = 0.67 CPU) @ 1492.54/s (n=1000) 8 Render : 11 wallclock secs ( 9.30 usr + 1.14 sys = 10.44 CPU) @ 95.82/s (n=1000)
Main project repository and bugtracker: https://github.com/hurricup/DTL-Fast
CPAN Testers reports: http://www.cpantesters.org/distro/D/DTL-Fast.html
Testers matrix: http://matrix.cpantesters.org/?dist=DTL-Fast
AnnoCPAN, Annotated CPAN documentation: http://annocpan.org/dist/DTL-Fast
CPAN Ratings: http://cpanratings.perl.org/d/DTL-Fast
Original Django templating documentation: https://docs.djangoproject.com/en/1.8/topics/templates/
Other implementaion: http://search.cpan.org/~maluku/Dotiac-0.8/lib/Dotiac/DTL.pm
This module is published under the terms of the MIT license, which basically means "Do with it whatever you want". For more information, see the LICENSE file that should be enclosed with this distributions. A copy of the license is (at the time of writing) also available at http://www.opensource.org/licenses/mit-license.php.
Copyright (C) 2014-2015 by Alexandr Evstigneev (hurricup@evstigneev.com)
To install DTL::Fast, copy and paste the appropriate command in to your terminal.
cpanm
cpanm DTL::Fast
CPAN shell
perl -MCPAN -e shell install DTL::Fast
For more information on module installation, please visit the detailed CPAN module installation guide.