This is the template compiler used by The Erlang Content Management Framework and CMS Zotonic.
This compiler is a complete rewrite of the erlydtl fork used in Zotonic. In contrast with ErlyDTL the template_compiler generates small Erlang modules which are shared between different templates and sites.
Run make
.
To test, run: make test
To use in your project with rebar3, use the Hex package:
{deps, [
template_compiler
]}.
Or directly from git:
{deps, [
{template_compiler, {git, "https://github.com/zotonic/template_compiler", {branch, "master"}}}.
]}.
The template language is largely the same as Django Template Language with some additions:
- Runtime includes, all templates are compiled to separate BEAM modules
overrules
to override same-named templates- translation tags
{_ my translatable text _}
and{% include "a.tpl" arg=_"my translatable text" %}
The template_compiler is a runtime compiler. It compiles templates from source files to in-memory BEAM modules. Templates are re-compiled if the source file changes or new translation strings are loaded.
Templates are compiled to functions containinge entry point to all blocks.
A template like:
Hello
{% block a %}this is block a{% endblock %}
World
{% block b %}this is block b{% endblock %}
{% optional include "foo.tpl" %}
Will be compiled to an Erlang module with the following structure:
-module(tpl_7bae8076a5771865123be7112468b79e9d78a640).
-export([
render/3,
render_block/4,
timestamp/0,
blocks/0,
module/0,
extends/0,
includes/0,
filename/0,
mtime/0,
is_autoid/0,
runtime/0
]).
%% The main render function.
render(Args, BlockMap, Context) -> [ ... ].
%% Render functions per block, the 'Blocks' is a block trace used internally.
render_block(a, Vars, Blocks, Context) -> [ ... ];
render_block(b, Vars, Blocks, Context) -> [ ... ];
render_block(_, _Vars, _Blocks, _Context) -> <<>>.
%% Timestamp on module compilation (os:timestamp/0)
timestamp() -> {1574,685843,340548}.
%% Block defined in this template.
blocks() -> [ a, b ].
%% The module name
module() -> tpl_7bae8076a5771865123be7112468b79e9d78a640.
%% The template that this template extends on.
extends() -> undefined.
%% The templates that this template includes.
%% Includes which use variable names for the template name are not listed.
includes() -> [
#{
template => <<"foo.tpl">>,
line => 5,
column => 20,
method => optional, % normal | all | optional
is_catinclude => false
}
].
%% The filename of this template
filename() -> <<"foo/bar/a.tpl">>.
%% The modification time of the template file on compilation
mtime() - {{2023,9,28},{11,51,49}}.
%% Flag if the autoid ("#id") construct is used in this template.
is_autoid() -> false.
%% Runtime module this template is linked against.
runtime() -> template_compiler_runtime.
The module includes debug information so that stack traces show the correct template file and line number.
The template_compiler uses a runtime module which implements template lookup and vatiable resolution methods.
There is a default runtime module in template_compiler_runtime
.
Render a template to an iolist:
Vars = #{ <<"a">> => 1 }, % Template variables, use a map
Options = [], % Render and compilation options
Context = your_request_context, % Context passed to the runtime module and filters
{ok, IOList} = template_compiler:render("hello.tpl", Vars, Options, Context).
The template_compiler:render/4
function looks up the template, ensures it is compiled, and then
calls the compiled render function of the template or the templates it extends (which are also compiled etc.).
Use template_compiler:compile_file/3
and template_compiler:compile_binary/4
to compile a template file or in-memory binary to a module:
{ok, Module} = template_compiler:compile_binary(<<"...">>, Filename, Options, Context).
The Filename is used to reference the compiled binary.
Sometimes (for example when template lookups or translations are changed) it is necessary to check all templates if they need to be recompiled.
For this are the flush functions:
template_compiler:flush()
forces a check on all templates.template_compiler:flush_file(TemplateFile)
forces check on the given template.template_compiler:flush_context(Context)
forces check on templates compiled within the given request context.
The template_compiler parses the templates till their Form format and then calculates a checksum. If the checksum is not changed from the previous compilation then the Form is not compiled, sparing valuable processing time.
For more complete documentation see Zotonic: http://docs.zotonic.com/en/latest/developer-guide/templates.html
Below is a short list of tags and explanation of template include / extend.
The template_compiler
package doesn't include filters.
Variables are surrounded by {{
and }}
(double braces):
Hello, I’m {{ first_name }} {{ last_name }}.
When rendering this template, you need to pass the variables to it. If you pass “James” for
first_name
and “Bond” for last_name
, the template renders to:
Hello, I’m James Bond.
Instead of strings, variables can also be objects that contain attributes. To access the attributes, use dot notation:
{{ article.title }} was created by {{ article.author.last_name }}
The article
could have been passed as a proplist or map in the Vars for the template render function:
#{
<<"article">> => #{
<<"title">> => <<"My title"/utf8>>,
<<"author">> => #{
<<"id">> => 1234,
<<"last_name">> => <<"Janssen">>
}
}
}.
Expressions can use different values types:
- Variables (see above)
- Number:
123
- String:
"hello"
or'hello'
- Translatable string:
_"Hello"
(see below) - List:
[ 1, 2, 3 ]
- Map, using Elixir syntax:
%{ a: 1, b: 2 }
where the keys will become binary strings equivalent to the Erlang map:#{ <<"a">> => 1, <<"b">> => 2 }
- Map, using strings as keys:
%{ "foaf:name":"Hello" }
- Erlang atom: `a` (quoted using backticks)
- Tagged value list:
{mytag a=1 b=2}
, this translates to Erlang{mytag, [{a,1}, {b,2}]}
- Unique generated id:
#foo
- an unique prefix is used for each template - Model calls
m.rsc.foo
- Model calls with an optional argument
m.rsc.foo::bar
There are three ways translatable texts can be used.
As a tag embedded in the text of the template:
{_ This text is translatable _}
As double quoted string prefixed with a '_', where you could also use a string:
{% include "_a.tpl" title=_"Translatable text" %}
As a text with arguments:
{% trans "Hello {foo}, Bye" foo=author.name_full %}
Note: to show a {
or }
in a trans
tag text then double it to {{
.
The trans
tag is compiled to code so very efficient.
To get a list of all translatable texts in a template use:
template_compiler:translations(TemplateFilename).
This returns a list of all strings, which could be used to generate a .po file:
[
{<<"Hello {foo}, Bye">>, [], {<<"foo/bar.tpl">>, 1234, 5}}
].
Templates can be combined to re-use parts and keep everything manageable.
First a template can be included in another template:
Hello {% include "_name.tpl" id=foobar %}
The template (_name.tpl
in this case) will be included at the called spot.
All variables known at the spot of the include will be passed, with the addition of
the arguments of the include tags.
Note: in ErlyDTL the included template is compiled inline into the surrounding template. In template_compiler the included template is compiled as a separate module.
You can also have a base template which can be used as the basis of other templates. The base template should define some blocks that can be changed in the template that extends the base template:
{% extends "base.tpl" %}
{% block name %}Piet!{% endblock %}
Where the base.tpl
could be:
Hello {% block name %}...{% endblock %} world.
The {% extends "..." %}
tag must be the first tag in the template. Also any text
outside the block tags will be dropped. So be sure to surround replaceable parts in your
base templates with block tags.
There is a variation on extends
where the template extends on a same-named template
of a lower priority. This is heavily used in Zotonic to extend templates in other modules.
{% overrules %}
{% block name %}...{% endblock %}
The overrules
is used to make it explicit that this template overrules (or extends)
another template with the same name.
This defines a named portion of a template that can be replaced in a template that extends (or overrules) this template:
Some text
{% block myname %}
Some text in the block that might be replaced
{% endblock %}
And text after the block
Blocks can be nested:
Some text
{% block myname %}
Some text in the block that might be replaced
{% block nestedblock %}
Text in the nested block
{% endblock %}
And more text in the outer block
{% endblock %}
And text after the block
The blocks are compiled to separate functions. The template compiler uses the template extends chain to figure out which block-function to render from which extending template.
The {% inherit %}
tag van be used inside a block to render the same-named block in
the extended (or overruled) template.
If base.tpl is like:
this is {% block a %}the base{% endblock %} template
And a.tpl is like:
{% extends "base.tpl" %}
{% block a %}hello {% inherit %} world{% endblock %}
Then a.tpl
renders like:
this is hello the base world template
Conditionally show or hide parts of a template:
{% if somevar %}
True
{% elif othervar %}
Other var
{% else %}
False
{% endif %}
The elif
can also be written as elseif
Define a variable to be used within an enclosed part of the template:
{% with someexpr as v %}
Here v can be used as any other variable
{% endwith %}
This is useful for using the result of a complicated expression multiple times
or if a forloop
(see below) iterator needs to be used in overruled blocks or
included templates.
Loop over a list of values, printing a comma separated list:
{% for k in [ 1, 2, 3, 4 ] %}
{{ k }}{% if nor forloop.last %},{% endif %}
{% endfor %}
Loop over a list of tuples (eg. [ {a, 1}, {b, 2} ]
):
<table>
{% for key, value in someproplist %}
<tr>
<th>{{ key|escape }}</th>
<td>{{ value|escape }}</td>
</tr>
{% empty %}
<tr>
<td>{_ Sorry, no values _}</td>
</tr>
{% endfor %}
</table>
Within a forloop there is an iterator called forloop
with the following
properties:
forloop.first
true if this is the first iterationforloop.last
true if this is the last iterationforloop.counter
iteration counter, starts at1
forloop.counter0
iteratiomn counter, starts at0
forloop.revcounter
iteration counter, starts atN
, ends at1
forloop.revcounter0
iteration counter, starts atN-1
, ends at0
forloop.parentloop
theforloop
parameter of the enclosing for loop (if any)
NOTE the forloop
iterator is only available in the template that contains
the for
loop, it is not passed to any included or extending templates.
If you want to use it in a block or included template then explicitly assign it to
a variable using with
or include arguments.
If the template compiler does sees that the forloop
is not used inside the template
where the forloop is defined then the code for tracking the forloop iterators is not
generated.
Everything surrounded by the tag is excluded:
{% comment %}
Some explanation that will not be part of the compiled template
{% endcomment %}
Useful to disable parts of a template or add some explanatory texts.
To echo some part without rendering it:
{% raw %}
{{ hello }} {% this is }} %} echo'd as-is
{% endraw %}
Remove spaces between HTML tags:
a-{% spaceless %} <a> x<span>xxx </span> </a> {% endspaceless %}-b
Renders as:
a-<a> x<span>xxx </span></a>-b
Print a value in <pre>
tags, used to inspect variables or dump some value
when you are still writing the template:
{% print somexpr %}
The expression value will be printed using io_lib:format("~p", [ SomExpr ])
, then
escaped and surrounded with <pre>...</pre>
.
- A single trailing newline is stripped, from the template, if present.
- Other whitespace, like tabs, newlines and spaces are returned unchanged.
The template_compiler is released under the APLv2 license. See the file LICENSE for the exact terms.