Skip to content

zotonic/template_compiler

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Template Compiler for Erlang / Elixir

Test Join the chat at https://gitter.im/zotonic/zotonic

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.

Building

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"}}}.
]}.

Template Language

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" %}

Runtime Just in Time compilation

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.

Runtime Module

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.

How to use

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.

Force recompilation

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.

Template language and Tags

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

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">>
        }
    }
}.

Values and expressions

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

Translatable texts

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.

List translatable strings

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}}
].

Template include / extends

Templates can be combined to re-use parts and keep everything manageable.

Include

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.

Extends

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.

Block

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.

Inherit

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

Template compose

This includes a template, but also defines extra blocks for the template to overrule the blocks in the template.

It is like a nameless {% overrules %} template, directly defined in the template text at the spot of the include.

{% compose "a.tpl" what="moon" %}
{% block a %}{{ what }}{% endblock %}
{% endcompose %}

And a.tpl is like:

Hello {% block a %}world{% endblock %}, and bye.

Then the above renders:

Hello moon, and bye.

There is also a catcompose to use a catinclude:

{% catcompose "a.tpl" id what="moon" %}
{% block a %}{{ what }}{% endblock %}
{% endcompose %}

If tag

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

With tag

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.

For tag

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 iteration
  • forloop.last true if this is the last iteration
  • forloop.counter iteration counter, starts at 1
  • forloop.counter0 iteratiomn counter, starts at 0
  • forloop.revcounter iteration counter, starts at N, ends at 1
  • forloop.revcounter0 iteration counter, starts at N-1, ends at 0
  • forloop.parentloop the forloop 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.

Comment tag

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.

Raw tag

To echo some part without rendering it:

{% raw %}
    {{ hello }} {% this is }} %} echo'd as-is
{% endraw %}

Spaceless tag

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 tag

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>.

Whitespace Handling

  • A single trailing newline is stripped, from the template, if present.
  • Other whitespace, like tabs, newlines and spaces are returned unchanged.

License

The template_compiler is released under the APLv2 license. See the file LICENSE for the exact terms.