Simple test

This is the minimal example of using the library. This example is printing a basic HTML page with with a dynamic paragraph.

examples/templateengine_simpletest.py
 1from adafruit_templateengine import render_string
 2
 3
 4template = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>Hello</title>
 9    </head>
10    <body>
11        <p>Hello, {{ context.get("name") or "Anonymous User" }}!</p>
12    </body>
13</html>
14"""
15
16context = {"name": ""}  # Put your name here
17
18print(render_string(template, context))

Caching/Reusing templates

The are two ways of rendering templates:

  • manually creating a Template or FileTemplate object and calling its method

  • using one of render_... methods

By dafault, the render_... methods cache the template and reuse it on next calls. This speeds up the rendering process, but also uses more memory.

If for some reason the caching is not desired, you can disable it by passing cache=False to the render_... method. This will cause the template to be recreated on every call, which is slower, but uses less memory. This might be useful when rendering a large number of different templates that might not fit in the memory at the same time or are not used often enough to justify caching them.

examples/templateengine_reusing.py
 1from adafruit_templateengine import Template, render_string
 2
 3
 4template_string = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>Hello</title>
 9    </head>
10    <body>
11        <p>Hello, {{ context.get("name") or "Anonymous User" }}!</p>
12    </body>
13</html>
14"""
15
16other_template_string = r"""
17<footer>
18    <p>Goodbye, {{ context.get("name") or "Anonymous User" }}!</p>
19</footer>
20"""
21
22# Manually create a Template object
23template = Template(template_string)  # Creates a template object
24print(template.render({"name": "John"}))  # Reuses the Template object
25print(template.render({"name": "Alex"}))  # Reuses the Template object
26
27# Using the `render_string` function
28print(
29    render_string(template_string, {"name": "John"})
30)  # Creates a new Template object and saves it
31print(render_string(template_string, {"name": "Alex"}))  # Reuses the Template object
32
33
34# Using the `render_string` function, but without caching
35print(
36    render_string(other_template_string, {"name": "John"}, cache=False)
37)  # Creates a new Template object and does not save it
38print(
39    render_string(other_template_string, {"name": "Alex"}, cache=False)
40)  # Creates a new Template object a second time and does not save it

Expressions

Token {{ ... }} is used for evaluating expressions.

The most basic use of the template engine is to replace some parts of a template with values. Those values can be passed as a dict, which is often called context, to the template.

This functionality works very similarly to Python’s f-strings.

Every expression that would be valid in an f-string is also valid in the template engine.

This includes, but is not limited to:

  • mathemathical operations e.g. {{ 5 + 2 ** 3 }} will be replaced with "13"

  • string operations e.g. {{ 'hello'.upper() }} will be replaced with "HELLO"

  • logical operations e.g. {{ 1 == 2 }} will be replaced with "False"

  • ternary operator e.g. {{ 'ON' if True else 'OFF' }} will be replaced with "ON"

  • built-in functions e.g. {{ len('Adafruit Industries') }} will be replaced with "19"

Of course, values from the context can also be used in all of the above expressions.

Values are not directly available in templates, instead you should access them using the context.

examples/templateengine_expressions.py
 1from adafruit_templateengine import render_string
 2
 3
 4template = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>Expressions</title>
 9    </head>
10    <body>
11        Your name is {{ context["name"] }} and it is {{ len(context["name"]) }} characters long.
12
13        If I were to shout your name, it would look like this: {{ context["name"].upper() }}!.
14
15        You are {{ context["age"] }} years old,
16        which means that you {{ "are not" if context["age"] <= 18 else "are" }} an adult.
17
18        You are also {{ 100 - context["age"] }} years away from being 100 years old.
19    </body>
20</html>
21"""
22
23context = {
24    "name": "John Doe",
25    "age": 23,
26}
27
28print(render_string(template, context))

if statements

Token {% if ... %} ... {% endif %} is used for simple conditional rendering. It can be used with optional {% elif ... %} and {% else %} tokens, with corresponding blocks.

Any Python logical expression can be used inside the {% if ... %} and {% elif ... %} token.

This includes, but is not limited to:

  • == - equal

  • != - not equal

  • < - less than

  • > - greater than

  • <= - less than or equal

  • >= - greater than or equal

  • in - check if a value is in a sequence

  • not - negate a logical expression

  • and - logical and

  • or - logical or

  • is - check if two objects are the same

examples/templateengine_if_statements.py
 1from adafruit_templateengine import render_string
 2
 3
 4template = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>If statements</title>
 9    </head>
10    <body>
11        {% if context["is_admin"] %}
12            <p>You are an admin.</p>
13        {% elif context["is_user"] %}
14            <p>You are a user.</p>
15        {% else %}
16            <p>You are a guest.</p>
17        {% endif %}
18    </body>
19</html>
20"""
21
22context = {
23    "is_admin": True,
24    "is_user": True,
25}
26
27print(render_string(template, context))

for loops

Token {% for ... in ... %} ... {% endfor %} is used for iterating over a sequence.

Additionally, {% empty %} can be used to specify a block that will be rendered if the sequence is empty.

examples/templateengine_for_loops.py
 1from adafruit_templateengine import render_string
 2
 3
 4template = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>For loops</title>
 9    </head>
10    <body>
11
12        Shopping list:
13        <ul>
14            {% for item in context["items"] %}
15                <li>{{ item["name"] }} - ${{ item["price"] }}</li>
16            {% empty %}
17                <li>There are no items on the list.</li>
18            {% endfor %}
19        </ul>
20    </body>
21</html>
22"""
23
24context = {
25    "items": [
26        {"name": "Apple", "price": 10},
27        {"name": "Orange", "price": 20},
28        {"name": "Lettuce", "price": 30},
29    ],
30}
31
32print(render_string(template, context))

while loops

Token {% while ... %} ... {% endwhile %} is used for rendering a block multiple times, until the condition is met.

By itself, this token is not very useful, most of the time, using {% for ... in ... %} is a better choice. The {% exec ... %} token, which is described later, can be used to modify the varaible which is used in the condition, thus allowing for breaking out of the loop.

Saying that, even without using {% exec ... %}, {% while ... %} can be used by itself:

examples/templateengine_while_loops.py
 1from random import randint
 2
 3from adafruit_templateengine import render_string
 4
 5
 6template = r"""
 7<!DOCTYPE html>
 8<html>
 9    <head>
10        <title>While loops</title>
11    </head>
12    <body>
13        Before rolling a 6, the dice rolled:
14        <ul>
15        {% while (dice_roll := context["random_dice_roll"]()) != 6 %}
16            <li>{{ dice_roll }}</li>
17        {% endwhile %}
18        </ul>
19    </body>
20</html>
21"""
22
23context = {"random_dice_roll": lambda: randint(1, 6)}
24
25print(render_string(template, context))

Including templates from other files

When writing a template, it is often useful to split it into multiple files.

Token {% include ... %} is used for including templates from other files. There is no support for dynamic includes, only static or hardcoded paths are supported.

This is often used to e.g. move a navigation bar of a website or footer into a separate file, and then include it in multiple pages.

examples/footer.html
1<footer>
2    Reusable footer that can be included in multiple pages
3</footer>
examples/base_without_footer.html
 1<!DOCTYPE html>
 2<html>
 3  <head>
 4    <title>Includes</title>
 5  </head>
 6
 7  <body>
 8    <nav>Navigation links</nav>
 9
10    <main>Main content of the page</main>
11
12    {% include "./examples/footer.html" %}
13  </body>
14</html>
examples/templateengine_includes.py
1from adafruit_templateengine import render_template
2
3
4print(render_template("./examples/base_without_footer.html"))

Blocks and extending templates

Sometimes seaprating different parts of a template into different files is not enough. That is where template inheritance comes in.

Token {% block ... %} ... {% endblock ... %} is used for defining blocks in templates, note that block must always have a name and end with a corresponding named endblock tag.

The only exception from this are block defined in a top level template (the one taht does not extend any other template). They are allowed to nothave a endblock tag.

A very common use case is to have a base template, which is then extended by other templates. This allows sharing whole layout, not only single parts.

examples/child.html
 1{% extends "./examples/parent_layout.html" %}
 2
 3{% block title %} {{ block.super }} - Cart {% endblock title %}
 4
 5{% block content %}
 6Items in cart:
 7<ul>
 8    {% for item in context["items"] %}
 9        <li>{{ item["name"] }} - ${{ item["price"] }}</li>
10    {% empty %}
11        <li>There are no items on the list.</li>
12    {% endfor %}
13</ul>
14{% endblock content %}
examples/parent_layout.html
 1<!DOCTYPE html>
 2<html>
 3
 4<head>
 5    <title>{% block title %}Online Shop{% endblock title %}</title>
 6</head>
 7
 8<body>
 9    {% block content %}
10
11    {% block footer %}
12</body>
13
14</html>
examples/templateengine_blocks_extends.py
 1from adafruit_templateengine import render_template
 2
 3context = {
 4    "items": [
 5        {"name": "Apple", "price": 10},
 6        {"name": "Orange", "price": 20},
 7        {"name": "Lettuce", "price": 30},
 8    ],
 9}
10
11print(render_template("./examples/child.html", context))

In the above example, {% block footer %} will be removed, as the child template does not provide any content for it. On the other hand {% block title %} will contain both the content from the parent template and the child template, because the child template uses {{ block.super }} to render the content from the parent template.

Executing Python code in templates

It is also possible to execute Python code in templates. This can be used for e.g. defining variables, modifying context, or breaking from loops.

examples/templateengine_exec.py
 1from adafruit_templateengine import render_string
 2
 3
 4template = r"""
 5<!DOCTYPE html>
 6<html>
 7    <head>
 8        <title>Executing Python code</title>
 9    </head>
10    <body>
11
12        {% exec name = "jake" %}
13        We defined a name: {{ name }}</br>
14
15        {% exec name = (name[0].upper() + name[1:].lower()) if name else "" %}
16        First letter was capitalized: {{ name }}</br>
17
18        {% exec name = list(name) %}
19        Not it got converted to a list: {{ name }}</br>
20
21        {% exec name = list(reversed(name)) %}
22        And reverse-sorted: {{ name }}</br>
23
24        {% for letter in name %}
25            {% if letter != "a" %}
26                {% if letter == "k" %}
27                    Skip a letter... e.g. "{{ letter }}"</br>
28                    {% exec continue %}
29                {% endif %}
30                We can iterate over it: "{{ letter }}"</br>
31            {% else %}
32                And break the loop when we find an "a" letter.</br>
33                {% exec break %}
34            {% endif %}
35        {% endfor %}
36    </body>
37</html>
38"""
39
40print(render_string(template))

Notice that varaibles defined in {% exec ... %} are do not have to ba accessed using context, but rather can be accessed directly.

Comments in templates

Template engine supports comments that are removed completely from the rendered output.

Supported comment syntaxes:

  • {# ... #} - for single-line comments

  • {% comment %} ... {% endcomment %} - for multi-line comments

  • {% comment "..." %} ... {% endcomment %} - for multi-line comments with optional note (both " and ' are supported)

examples/comments.html
 1<!DOCTYPE html>
 2<html>
 3<head>
 4    <meta charset="UTF-8" />
 5    <title>Example of comments in templates</title>
 6</head>
 7<body>
 8    Nothing below this line will be rendered
 9
10    {# This is a line comment #}
11
12    {% comment %}
13        This is a block comment without optional note
14    {% endcomment %}
15
16    {% comment "Optional note" %}
17        This is a block comment with optional note
18    {% endcomment %}
19</body>
20</html>
examples/templateengine_comments.py
1from adafruit_templateengine import render_template
2
3
4print(render_template("./examples/comments.html"))

Autoescaping unsafe characters

Token {% autoescape off %} ... {% endautoescape %} is used for marking a block of code that should be not be autoescaped. Consequently using {% autoescape on %} ... does the opposite and turns the autoescaping back on.

By default the template engine will escape all HTML-unsafe characters in expressions (e.g. < will be replaced with &lt;).

Content outside expressions is not escaped and is rendered as-is.

examples/autoescape.html
 1<!DOCTYPE html>
 2<html>
 3
 4<head>
 5    <meta charset="UTF-8" />
 6    <title>
 7        Example of autoescaping unsafe characters in HTML
 8    </title>
 9</head>
10
11<body>
12    This would be a {{ "<b>bold text</b>" }}, but autoescaping is turned on, so all
13    unsafe characters are escaped.
14
15    {% autoescape off %}
16        This is a {{ "<b>bold text</b>" }}, because autoescaping is turned off in this block.
17
18        {% autoescape on %}
19            And again, this is not a {{ "<b>bold text</b>" }},
20            because in this block autoescaping is turned on again.
21        {% endautoescape %}
22    {% endautoescape %}
23</body>
24
25</html>
examples/templateengine_autoescape.py
1from adafruit_templateengine import render_template
2
3# By default autoescape is enabled for HTML entities
4print(render_template("./examples/autoescape.html"))