| Author: | Roland Koebler (rk at simple-is-better dot org) |
|---|---|
| Website: | http://www.simple-is-better.org/template/ |
| Date: | 2009-01-07 |
Please don't hesitate to contact me if you have any comments/suggestions/etc.
Template engines are tools to separate program-logic and presentation into two independent parts [1]. (see i.e. articles in wikipedia-en, wikipedia-de)
This makes the development of both logic and presentation easier, improves flexibility and eases modification and maintenance.
| [1] | By the way: Splitting things into independent parts is always a very good idea (although unfortunately mostly neglected). |
So, with a template engine the programmer should separate the presentation (=view) from the logic (=model). It is very important to strictly separate these two parts! Otherwise these parts won't be independent anymore, and you will lose all advantages of a template engine!
Unfortunately, the template engine itself cannot completely enforce this separation without some serious restrictions on the view (see below). And -- at least in my opinion -- the template-engine should not restrict the design of the view or even make some designs impossible.
Since the separation cannot be done automatically, here are some rules:
All data from the model must be display-/layout-independent. Display/layout-information in the data is forbidden.
Make all computations of the model in the model.
The view must only perform computations for view-specific and model-independent "things".
But in reality, there are different kinds of template-engines, with different approaches. I'll group them into 4 categories (although many of the existing template-engines are somewhere between these categories):
substitution only:
The template only contains placeholders which are replaced by data. No loops, no conditions etc. are possible in the template. This is the simplest but also least powerful kind of templates.
Examples: printf-formatstring, python's string.Template, Templayer.
It should be clear that this is insufficient for most cases.
substitution + if-defined + loop-over-data + recursive macros:
The template can contain:
This is powerful enough for some websites. And it cuts down the power of the template, to prevent that anything which belongs to the model can be done in the template/view.
This approach is described in detail in an interesting paper, named Enforcing Strict Model-View Separation in Template Engines. Although I don't agree to many conclusions of the author (and think that these templates are definitely not powerful enough), it may be worth reading.
BUT: This approach puts some serious restrictions on the view: Some designs simply can't be done (see Why if/for/...) -- or can only be done if you put parts of the presentation into the model code, which is a no-go. This often makes this approach unusable.
Examples: StringTemplate (the template of the above paper), ...
substitution + conditionals + loops + macros + "embedded expressions" with restricted access:
In addition to the above, the template may also contain "free" loops, tests of variable-values, and has "expressions of a embedded language" (e.g. for calculations, formatting etc.). But this "embedded language" can only access a restricted set of variables/functions; it especially cannot access or modify the model.
This is powerful enough for all cases. And it prevents direct access and modification of the model in the template. It can't prevent that tests, calculations etc. which belong to the model are done in the template -- this is in the responsibility of the programmer/designer. But that's the price for a full flexible, unrestricted view.
Examples: unfortunately not many, but e.g. my pyratemp
unrestricted templates, i.e. unrestricted embedded code:
Many template-engines simply include a full-powered programming language without any restrictions into the template. But this has the drawback that even the model can be modified in the template, which can completely countermine the model-view-separation.
Examples: Mako, Cheetah, TemplateToolkit, EmPy, ...
There are many people who think that the "category 2" above is sufficient. Unfortunately, this is not the case. There are a lot of things which clearly belong to the view, but are not possible without variable-testing, for-loops, calculations, formatting etc.
This can be demonstrated best with a few examples. Note that these examples are not constructed, but really occurred in existing templates:
"free" for-loops:
Imagine a fixed form, with e.g. a table which should always have 10 rows, no matter how much data exists. If there is less data, the table will have some empty rows. If there is more data, there will be 2 (or more) tables.
This probably isn't needed very often in websites, but definitely occurs in fixed formats, e.g. in LaTeX.
Or simply take an enumerated list like this:
Value |
|
|---|---|
1 |
one |
2 |
two |
3 |
three |
4 |
four |
The enumeration and enumeration-style here belongs to the view.
(Note: This is possible in some "category-2-template-engines", but only due to some "magic-variables" like loop-counters. But it then only works for "1,2,3,..." and not for "10,20,30,..." or "I,II,III,...".)
formatting:
calculations:
Imagine a bill (e.g. in LaTeX) with a variable number of items, which may be broken into several sheets of paper. The calculation of a subtotal at the end of each page clearly belongs to the view.
All these examples clearly belong to the view, but are not possible with the pure "category 2" approach. And these are only some examples -- there are a lot more...
This is why such "category 2"-templates often get extended bit by bit, until they end in "category 3 (or 4)", but then have an inelegant concept.
Here's a short description of how a template-engine should look like in my opinion:
stand-alone (i.e. work without a webserver)
simple, small, lightweight, easy to use, well-documented and extensible
clear template syntax ("There should be one -- and preferably only one -- obvious way to do it." [2])
completely Unicode
good syntax-error-checking and good error-messages, to make the writing and debugging of templates easy
fast
basic functionality from "category 3" above):
- placeholders/string-substitution
- conditionals
- loops
- macros (with parameters)
- restricted "embedded expressions" (formatting, arithmetics etc.)
additional functionality:
- inclusion of other templates (for reusability and "things" which are used in several templates)
- escaping-mechanism (e.g. for html: escape < > & ' ")
- (MAYBE) definition of the result-encoding
Since unfortunately I didn't find any existing template-engine which fitted these needs, I created an own template-engine which accomplishes most of the above: pyratemp
| [2] | from: The Zen of Python |
How fast is a template-engine? That's a difficult question, and the answer always depends on the used benchmark, since a benchmark can't tell us how fast a template-engine is, but only how fast one specific implementation of one specific template is. So, don't rely too much on benchmarks.
Additionally, the speed of a template-engine normally isn't a problem -- except if the template-engine is really slow.
Here are some benchmarks:
benchsimple: This is a simple template-benchmark, which originally was posted on a python-forum, and then modified and extended by myself.
It creates a single html-page with a table with 50 entries.
A bench-suite of Mako or evoque, which were adapted from one included with Genshi. But note that this benchmark only measures the pure rendering-time, and completely omits the time needed to parse/compile/... the template!
"Benchsimple" creates a simple html-page with a navigation-bar and a table with 50 entries (5 rows * 10 columns). It only uses a few template-features that are supported by all templates (escaped substitution, conditionals, loops), and so there's no Unicode, no included templates, no arithmetics, no template-defined formatting etc.
I've tested pyratemp, cubictemp, Jinja, Cheetah, Mako, EmPy, evoque and SimpleTAL, and also compared them to manually written code.
But note that the following "results" can only give you a hint how fast a specific template-engine might be.
Results:
| Template-Engine | Version | correct result | import | complete | parse only | render only |
|---|---|---|---|---|---|---|
| Cheetah | 2.0rc7 | yes | 0.0028 | 0.84 | 0.054 | 0.70 |
| cubictemp | 0.4 | yes | 0.0012 | 2.0 | -- | 1.8 |
| cubictemp [3] | 2.0 | yes | 0.0013 | 2.0 | 0.72 | 1.2 |
| pyratemp | 0.1 | yes | 0.0011 | 2.3 | 1.0 | 1.2 |
| evoque [3] | 0.3 | yes | 0.0030 | 3.6 | 1.0 | 2.0 |
| SimpleTAL | 4.1-6 | no [4] | 0.0060 | 7.2 | 2.5 | 4.7 |
| EmPy | 3.3-6 | yes | 0.0012 | 8.8 | -- | -- |
| Jinja | 0.7 | partly [5] | 0.0029 | 13.2 | 10.2 | (2.9) |
| Mako | 0.1.8-1 | yes | 0.0030 | 25.0 | 23.8 | 0.74 |
| [3] | (1, 2) tested with Python 2.5 |
| [4] | SimpleTAL: The result had different whitespace. Although this is probably not a problem for generating HTML, it is a problem for other documents. |
| [5] | Jinja produces two different results if it is rendered several times, because {% cycle ...%} alternately uses 'class=row1' and 'class=row2' for the first row. |
I also wrote some python-code, which also creates the same page, but without a template-engine. This shows how fast the template-engines are compared to manually written Python-code. Additionally, I modified the code to optionally don't escape special characters, to use the variables in the substitutions directly without eval() and to use psyco. This shows which part takes how much time.
| eval() | escaping | psyco | correct result | time | |
|---|---|---|---|---|---|
| python hand-coded | yes | yes | -- | yes | 0.53 |
| python hand-coded | yes | yes | yes | yes | 0.34 |
| python hand-coded | no | yes | -- | (yes) | 0.38 |
| python hand-coded | no | yes | yes | (yes) | 0.18 |
| python hand-coded | yes | no | -- | no | 0.32 |
| python hand-coded | yes | no | yes | no | 0.27 |
| python hand-coded | no | no | -- | no | 0.18 |
| python hand-coded | no | no | yes | no | 0.12 |
There are very many template-engines, and every day someone "invents" another...
Additionally, I've created the same html-page using different template-engines in Benchsimple, so look there to get a first impression how these template-engines look like.
Since I haven't tested every template-engine in detail, some of the fields below are empty, and I probably haven't found some hidden features of some template-engines. So, please tell me if anything below is wrong.
| Template-Engine | code | features | syntax | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| name | version | language | size | lines-of-code | license | category | XML-based | Unicode | error-msg. | escaping | undef. vars | embedded code | embedded expr. | sandbox | macros | include | variable access | substitution | escaped | conditionals | loops | macros | include | inheritance | ... | block-end |
| pyratemp | 0.1.2 | Python | 42 kB | 486 | MIT-like | 3 | no | + | + | + | complain (optionally replace) | no | restricted Python | + | + | + | v, v[i], v["key"] | $!...!$ | @!...!@ | <!--(if ..)-->
<!--(elif ..)-->
<!--(else)-->
|
<!--(for .. in ..)-->
<!--(else)-->
|
<!--(macro ..)--> | <!--(include)--> | via include + macro | <!--(end)--> | |
| cubictemp | 0.4 | Python | 8 kB | 120 | MIT-like | 4 | no | -- | -- | + (HTML) | complain | no | Python | -- | + | -- | v, v[i], v["key"] | $!...!$ | @!...!@ | @!if .. then .. else ..!@ | <!--(for .. in ..)--> | <!--(block ..)--> | -- | -- | <!--(end)--> | |
| cubictemp | 2.0 | Python 2.5 | 9 kB | 185 | MIT-like | 4 | no | -- | -- | + (HTML) | complain | no | Python | -- | + | -- | v, v[i], v["key"] | $!...!$ | @!...!@ | @!.. if .. else ..!@ | <!--(for .. in ..)--> | <!--(block ..)--> | -- | -- | <!--(end)--> | |
| Mako | 0.1.8-1 | Python | 148 kB | 2530 | MIT-like | 4 | no | + | o | + | complain | Python | Python | -- | + | + | v, v[i], v["key"] | ${...} | ${...|x} | % if ..
% elif ..
% else
|
% for .. in ..: | <%def ..> | <%include ../> | <%inherit ../> | % end.., </%..> | |
| Cheetah | 2.0~rc7-1 | Python | 703 kB | 11295-16955 | MIT-like | 4 | no | + | -- | o | complain | Python | Python | -- | + | + | v, v[i], v["key"] | $... ${...} | #filter WebSafe | #if ..
#elif ..
#else
#unless
|
#for .. in ..
#repeat, #while
#break, #continue
|
#def .. | #include .. | #import, #extends, #block, #implements | #try, #except, #return .. | #end .. |
| Jinja | 0.7 | Python | 100 kB | 1789 | GPL | 2(-4) | no | o | -- | o | ignore | no | own language | + | + | + | v, v.i, v.key | {{ .. }} | {{.|escapexml}} | {% if .. %}
{% else %}
|
{% for .. in .. %}
{% range .. %}
|
{% prepare ..%}
{% call ..%}
|
{% include ..%}
{% require ..%}
|
{% extends %}
{% block ..%}
{% marker ..%}
|
{% end.. %} | |
| Jinja | 1.1 | Python | 283 kB | 3436 | BSD | 4 | no | + | o | ignore or complain | no | own lang. + Python | + | + | + | v, v[i], v["key"], v.key | {{ .. }} | {{..|e} | {% if .. %}
{% elif .. %}
{% else %}
|
{% for .. in .. %}
{% else %}
|
{% macro ..%} | {% include ..%} | {% extends ..%}
{% block ..%}
|
{% end.. %} | ||
| Jinja | 2.0 | Python | ||||||||||||||||||||||||
| evoque | 0.3 | Python 2.5 | 123 kB | 932 | AFL | 3 | no | + | -- / o | + | ignore / complain / ... | no | (restricted) Python | o [6] | + | + | v, v[i], v["key"] | ${...} | $if{..}
$elif{..}
$else{..}
|
$for{.. in ..}
$else
|
$begin{...}
$end{...}
|
$evoque{...} | $evoque{..}
$overlay{..}
|
... | $fi, $rof | |
| EmPy | 3.3-6 | Python | 112 kB | 2387 | LGPL | 4 | no | o | -- | -- / o | complain | Python | Python | -- | Python | Python | via Python | @(...) | via Python | @[if ..]
@[elif ..]
@[else ..]
|
@[for .. in ..]
|
@{def ..} | via Python | ... | @[end ..] | |
| TemplateToolkit | 2.? | Perl | GPL / Artistic | 4 | no | Perl [% PERL %] | Perl | -- | + | [% .. %] | [% IF ..%]
[% ELIF ..%]
[% ELSE ..%]
[% UNLESS ..%]
[% SWITCH%] [% CASE %]
|
[% FOREACH ..%]
[% WHILE ..%]
[% LAST %] [% NEXT %]
|
[% MACRO %] | [% WRAPPER ..%] [% BLOCK ..%] [% PROCESS ..%] | [% TRY %] [% CATCH %] [% RETURN %] | [% END %] | ||||||||||
| StringTemplate | 2 | no | no | |||||||||||||||||||||||
| pyTemple | 0.1 | Python | 59 kB | 927 | LGPL | 2-4 | no | |||||||||||||||||||
| pyt | 0.2.1 | Python | 10 kB | 215 | LGPL | 4 | no | Python | %(VAR)F | |||||||||||||||||
| htmltmpl | 1.22 | Python/PHP | 59 kB | 729 | GPL | no | ||||||||||||||||||||
| Templayer | 1.4 | Python | 21 kB | 386 | LGPL | 1 | no | |||||||||||||||||||
| texttemplate | 0.2.0 | Python | 16 kB | 223 | MIT-like | |||||||||||||||||||||
| HTMLtemplate | 1.5.0 | Python | 30 kB | 397 | MIT-like | 1-2 | yes | |||||||||||||||||||
| XML/XSLT | yes | |||||||||||||||||||||||||
| Genshi | 0.3.4-1 | Python | 179 kB | 2753 | BSD-like | yes | ||||||||||||||||||||
| Kid | 0.9.3-1 | Python | 107 kB | 3474 | MIT-like | 4 | yes | |||||||||||||||||||
| SimpleTAL | 4.1-6 | Python | 104 kB | 1953 | BSD-like | yes | ||||||||||||||||||||
| OpenTAL | yes | |||||||||||||||||||||||||
| ClearSilver | ||||||||||||||||||||||||||
| N:PyTpl | 0.5.1 | Python | MIT-like | |||||||||||||||||||||||
| XTemplate4Python | 0.1.0 | Python | LGPL | |||||||||||||||||||||||
Can (Python-)statements be included in the template?
Can (Python-)expressions be included in the template?
| [6] | The sandbox of evoque is disabled by default. |
pyratemp is my own template-engine. It's probably (one of) the smallest complete template-engines [7], and it uses a very small set of special syntax in the templates. Both is good, because it reduces complexity and the probability of bugs and leads to a easy-to-use and intuitive user-interface.
Additionally, it is quite fast (although not optimized for speed), uses a (pseudo-)sandbox and produces exceptional good error-messages (e.g. pyratemp.TemplateSyntaxError: line 18, col 1: invalid keyword 'fo'), which is extremely useful.
pyratemp was inspired by cubictemp. Since there were some essential features missing in cubictemp (see below), after trying to extend cubictemp, I decided to write my own template-engine from scratch, which is nearly as small and simple as cubictemp, but without its weaknesses.
More details can be found on my pyratemp-page.
| [7] | pyratemp consists of about 500 lines-of-code. And the whole sourcecode is easy to understand and documented by about 500 lines of docstrings! |
cubictemp is probably the smallest of all template-engines. I completely agree to a statement on its homepage:
There are many large, over-designed Python templating systems out there.
Unfortunately, there are some essential features missing in cubictemp, like Unicode-support, inclusion of other templates, good/helpful error-messages, and some kind of a sandbox. Additionally, its conditionals (if/else) are weak, since the don't have a "elif" and can't contain template-syntax in its branches.
Jinja 0.7 was probably the best template-engine without embedded Python-expressions. And it included a sandbox, so a template could not do "bad things". But its embedded expressions were not powerful enough for many purposes, and so embedded Python-expressions were added in later releases. (Note that this is exactly what I predicted at the end of Why if/for/calculations etc. are necessary in the View.)
But although I haven't tested Jinja 2.0 yet and don't know how good its sandbox is, Jinja may be worth a look.
I recently found evoque, and was surprised that it has many things in common with pyratemp, and nearly has everything I described a template-engine should have (see What a template-engine needs).
But after reading parts of its documentation and a short test, I also found a few things I don't like -- especially in comparison with pyratemp:
It's really counterintuitive to generate a document with an exact amount of whitespace. You can find an example in my benchsimple-code, where you can compare the whitespace in the template with the ones in the result. (Note that in the benchsimple-example, the whitespace before "</ul>" is ignored, and instead the whitespace before "$rof" of the for-loop above is used for "</ul>"!)
Although that's probably not a problem when creating HTML/XML, it is when creating other documents.
"sandbox": By default, the sandbox is disabled, and I don't know if this sandbox is secure or not.
But a look into the sourcecode showed that the programmer forbids known-unsafe functions instead of explicitly allowing only known-safe functions, which is definitely the wrong way and leaves a bad impression.
And it's code base is larger and worse documented than pyratemp. ;)