Skip to content

Template Engine

The framework includes a built-in template engine for rendering text (HTML, emails, any text format) with dynamic data taken from a JSON document.

How It Works

A template is a text file containing special tags. The data source is a json_doc_t*, and the file itself is read from a storage declared in config.json.

Parsing and compiling a template into a tag tree happens once; the result is cached in viewstore keyed by the file path. Subsequent render() calls with the same path reuse the compiled tree, so repeated rendering stays fast.

Storage Configuration

Templates live in a regular storage (filesystem or s3). The storage name is then passed to the rendering functions.

json
{
    "storages": {
        "views": {
            "type": "filesystem",
            "root": "/var/www/app/views"
        }
    }
}

API

render()

c
#include "view.h"

char* render(json_doc_t* document, const char* storage_name, const char* path_format, ...);

Renders a template and returns the resulting string.

Parameters

  • document — JSON document with data for the template.
  • storage_name — name of the storage where the template is located.
  • path_format — path to the template file inside the storage. Supports printf-style formatting through variadic arguments.

Return value

Pointer to a string with the rendered result, or NULL on error (file not found, syntax error). The memory must be freed with free().

send_view()

A method on the response object — renders a template and immediately sends the result to the client.

c
void (*send_view)(struct httpresponse* response, json_doc_t* document,
                  const char* storage_name, const char* path_format, ...);
c
ctx->response->send_view(ctx->response, document, "views", "/index.tpl");

Syntax

The engine recognizes three kinds of tags:

TagPurposeExample
{{ ... }}Output a variable value{{ user.name }}
{% ... %}Control structures (if, for){% if active %}
{* include ... *}Include another template{* include /header.tpl *}

Variables

A value is printed with {{ }}. Nested fields are accessed with a dot, and array elements by index in square brackets.

html
<p>Hello, {{ name }}!</p>
<p>Email: {{ user.email }}</p>
<p>First item: {{ items[0] }}</p>
<p>First user's name: {{ users[0].name }}</p>

Strings and numbers only

The values of string and numeric JSON fields are printed. If a value is missing or is an object/array, nothing is output.

Conditions

html
{% if is_authenticated %}
    <p>Welcome, {{ user.name }}!</p>
{% elseif is_guest %}
    <p>You are logged in as a guest</p>
{% else %}
    <p>Please log in</p>
{% endif %}

Condition inversion with !:

html
{% if !is_authenticated %}
    <a href="/login">Log in</a>
{% endif %}

A condition is true when the variable equals boolean true. The condition expression supports nested field and index access just like regular variables:

html
{% if users[0].authorized %}
    {{ users[0].name }} is authorized
{% endif %}

Loops

Iterating over an array:

html
<ul>
{% for product in products %}
    <li>{{ product.name }} — {{ product.price }}</li>
{% endfor %}
</ul>

With access to the index (the second name after the comma):

html
<ol>
{% for product, i in products %}
    <li>{{ i }}: {{ product.name }}</li>
{% endfor %}
</ol>

If the index name is omitted, it defaults to index:

html
{% for product in products %}
    <p>#{{ index }}: {{ product.name }}</p>
{% endfor %}

Loops also iterate over objects — in that case the index holds the key name (a string), and the element holds the property value:

html
{% for value, key in settings %}
    <p>{{ key }} = {{ value }}</p>
{% endfor %}

Inside a loop, variables named after the element and the index become available; their fields are accessed like any other variable.

Including Templates

Including another file is done with the {* include ... *} tag. The path is given as-is, without quotes — it is a path within the template storage.

html
{* include /partials/header.tpl *}

<main>
    <h1>{{ title }}</h1>
</main>

{* include /partials/footer.tpl *}

Include depth limit

Include depth is limited to 100 levels. Circular includes (a template including itself unconditionally) will fail at parse time.

Usage Examples

Simple Rendering

c
#include "http.h"
#include "view.h"

void home(httpctx_t* ctx) {
    json_doc_t* doc = json_root_create_object();
    json_token_t* root = json_root(doc);

    json_object_set(root, "title", json_create_string("Home"));
    json_object_set(root, "username", json_create_string("John"));

    ctx->response->send_view(ctx->response, doc, "views", "/pages/home.tpl");

    json_free(doc);
}

Rendering with a Formatted Path

The path supports printf-style formatting — handy when the template name depends on input:

c
char* html = render(doc, "views", "/pages/%s.tpl", page_name);

if (html == NULL) {
    ctx->response->send_default(ctx->response, 404);
    return;
}

ctx->response->header_add(ctx->response, "Content-Type", "text/html; charset=utf-8");
ctx->response->send_data(ctx->response, html);
free(html);

Product List

c
void products_list(httpctx_t* ctx) {
    json_doc_t* doc = json_root_create_object();
    json_token_t* root = json_root(doc);

    json_object_set(root, "title", json_create_string("Products"));

    json_token_t* products = json_create_array(doc);

    json_token_t* p1 = json_create_object(doc);
    json_object_set(p1, "name", json_create_string("Laptop"));
    json_object_set(p1, "price", json_create_number(50000));
    json_array_append(products, p1);

    json_token_t* p2 = json_create_object(doc);
    json_object_set(p2, "name", json_create_string("Phone"));
    json_object_set(p2, "price", json_create_number(30000));
    json_array_append(products, p2);

    json_object_set(root, "products", products);

    ctx->response->send_view(ctx->response, doc, "views", "/pages/products.tpl");

    json_free(doc);
}

Template /pages/products.tpl:

html
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
    <h1>{{ title }}</h1>

    {% if products %}
        <table>
            <tr><th>#</th><th>Name</th><th>Price</th></tr>
            {% for product, i in products %}
            <tr>
                <td>{{ i }}</td>
                <td>{{ product.name }}</td>
                <td>{{ product.price }}</td>
            </tr>
            {% endfor %}
        </table>
    {% else %}
        <p>No products found</p>
    {% endif %}
</body>
</html>

Rendering with Model Data

c
void user_profile(httpctx_t* ctx) {
    array_t* params = array_create();
    mparams_fill_array(params, mparam_int(id, 1));
    user_t* user = user_get(params);
    array_free(params);

    if (user == NULL) {
        ctx->response->send_default(ctx->response, 404);
        return;
    }

    json_doc_t* doc = json_root_create_object();
    json_token_t* root = json_root(doc);

    json_object_set(root, "user", model_to_json(user, NULL));
    json_object_set(root, "title", json_create_string("User Profile"));

    user_free(user);

    ctx->response->send_view(ctx->response, doc, "views", "/pages/profile.tpl");

    json_free(doc);
}

Nested Loops

Suppose there is a document with users and their tasks:

json
{
    "company": "My Company",
    "users": [
        {
            "name": "Alex",
            "authorized": true,
            "tasks": ["Task 1", "Task 2", "Task 3"]
        },
        {
            "name": "Bonnie",
            "authorized": false,
            "tasks": ["Task 4", "Task 5"]
        }
    ]
}

A template iterating over users and their tasks:

html
{* include /header.tpl *}

Users of {{ company }}:
{% for user, i in users %}
    {{ i }}. {{ user.name }}
    Authorized: {% if user.authorized %}yes{% else %}no{% endif %}
    Tasks:
    {% for task, j in user.tasks %}
        - {{ j }}: {{ task }}
    {% endfor %}
{% endfor %}

The outer loop declares the element user and the index i; the inner loop declares the element task and the index j.

Template Organization

Recommended storage layout:

views/
├── layouts/
│   └── base.tpl
├── partials/
│   ├── header.tpl
│   └── footer.tpl
└── pages/
    ├── home.tpl
    ├── products.tpl
    └── profile.tpl

partials/header.tpl

html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
    <link rel="stylesheet" href="/css/style.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            {% if is_authenticated %}
                <a href="/profile">Profile</a>
                <a href="/logout">Logout</a>
            {% else %}
                <a href="/login">Login</a>
            {% endif %}
        </nav>
    </header>
html
    <footer>
        <p>&copy; 2024 Example App</p>
    </footer>
</body>
</html>

Escaping and Security

Values are not escaped automatically

The engine outputs string values verbatim, without HTML escaping. If data comes from user input and is inserted into HTML, it must be escaped beforehand — otherwise arbitrary HTML/JavaScript may execute (XSS).

Outputting values that may contain special characters (<, >, &, ") is only safe after sanitizing them on the handler side.

Released under the MIT License.