Tera Templating

Panopticon uses Tera as its templating engine. Tera provides a powerful, Jinja2-inspired syntax for variable interpolation, filters, and control structures.

How Templates Connect to Data

The ScalarStore holds all scalar values (strings, numbers, booleans, arrays, objects) and serves as the template context. Any value stored via a StorePath becomes available in templates using dot notation:

ScalarStore Contents:               Template Access:
=====================               ================

inputs.site_name = "My Site"    ->  {{ inputs.site_name }}
inputs.page_title = "Home"      ->  {{ inputs.page_title }}
config.debug = true             ->  {{ config.debug }}
data.load.row_count = 42        ->  {{ data.load.row_count }}

Basic Variable Interpolation

Simple Values

Access scalar values using double curly braces:

<h1>{{ inputs.site_name }}</h1>
<p>Processing {{ data.load.row_count }} records</p>

Nested Objects

Navigate into nested structures with dot notation:

#![allow(unused)]
fn main() {
// Stored as:
ObjectBuilder::new()
    .object("database", ObjectBuilder::new()
        .insert("host", "localhost")
        .insert("port", 5432))
    .build_scalar()
}
<!-- Template -->
Connecting to {{ config.database.host }}:{{ config.database.port }}

Array Access

Access array elements by index:

First item: {{ items.0 }}
Third item: {{ items.2 }}

Filters

Filters transform values using the pipe (|) syntax:

Built-in Filters

<!-- String manipulation -->
{{ name | upper }}              <!-- JOHN -->
{{ name | lower }}              <!-- john -->
{{ name | capitalize }}         <!-- John -->
{{ name | title }}              <!-- John Doe -->
{{ text | trim }}               <!-- removes whitespace -->
{{ text | truncate(length=20) }}

<!-- Number formatting -->
{{ price | round }}             <!-- 10 -->
{{ price | round(precision=2) }}<!-- 9.99 -->

<!-- Collections -->
{{ items | length }}            <!-- array length -->
{{ items | first }}             <!-- first element -->
{{ items | last }}              <!-- last element -->
{{ items | reverse }}           <!-- reversed array -->
{{ items | join(sep=", ") }}    <!-- comma-separated -->

<!-- Default values -->
{{ maybe_null | default(value="N/A") }}

<!-- JSON encoding -->
{{ object | json_encode() }}
{{ object | json_encode(pretty=true) }}

<!-- Escaping -->
{{ html_content | safe }}       <!-- no escaping -->
{{ user_input | escape }}       <!-- HTML escape -->

Filter Chaining

Chain multiple filters together:

{{ name | trim | upper | truncate(length=10) }}

Control Structures

Conditionals

{% if config.debug %}
    <div class="debug-panel">Debug mode enabled</div>
{% endif %}

{% if user.role == "admin" %}
    <a href="/admin">Admin Panel</a>
{% elif user.role == "editor" %}
    <a href="/edit">Edit Content</a>
{% else %}
    <span>Welcome, guest</span>
{% endif %}

Loops

Iterate over arrays:

<ul>
{% for item in inputs.nav_items %}
    <li><a href="{{ item.url }}">{{ item.label }}</a></li>
{% endfor %}
</ul>

Loop variables:

{% for item in items %}
    {{ loop.index }}      <!-- 1-indexed position -->
    {{ loop.index0 }}     <!-- 0-indexed position -->
    {{ loop.first }}      <!-- true if first iteration -->
    {{ loop.last }}       <!-- true if last iteration -->
{% endfor %}

Iterate over object key-value pairs:

{% for key, value in config.settings %}
    {{ key }}: {{ value }}
{% endfor %}

Template Inheritance

Tera supports template inheritance for building complex layouts:

Base Template (base.tera)

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
{% block header %}{% endblock %}
<main>
{% block content %}{% endblock %}
</main>
<footer>
    <p>Generated by Panopticon</p>
</footer>
</body>
</html>

Child Template (page.tera)

{% extends "base.tera" %}

{% block title %}{{ inputs.page_title }} - {{ inputs.site_name }}{% endblock %}

{% block header %}
{% include "header.tera" %}
{% endblock %}

{% block content %}
<article>
    <h2>{{ inputs.page_title }}</h2>
    <p>{{ inputs.page_content }}</p>
</article>
{% endblock %}

Include Template (header.tera)

<header>
    <h1>{{ inputs.site_name }}</h1>
    <nav>
        {% for item in inputs.nav_items %}
        <a href="{{ item.url }}">{{ item.label }}</a>
        {% endfor %}
    </nav>
</header>

Using Templates in Panopticon

TemplateCommand

The TemplateCommand renders Tera templates:

#![allow(unused)]
fn main() {
use panopticon_core::prelude::*;

let mut pipeline = Pipeline::new();

// Add input data
pipeline.add_namespace(
    NamespaceBuilder::new("inputs")
        .static_ns()
        .insert("site_name", ScalarValue::String("Panopticon Demo".into()))
        .insert("page_title", ScalarValue::String("Getting Started".into()))
        .insert("page_content", ScalarValue::String("Welcome!".into()))
        .insert("nav_items", ScalarValue::Array(vec![
            ObjectBuilder::new()
                .insert("label", "Home")
                .insert("url", "/")
                .build_scalar(),
            ObjectBuilder::new()
                .insert("label", "Docs")
                .insert("url", "/docs")
                .build_scalar(),
        ])),
).await?;

// Configure template command
let template_attrs = ObjectBuilder::new()
    .insert("template_glob", "/path/to/templates/**/*.tera")
    .insert("render", "page.tera")
    .insert("output", "/output/page.html")
    .insert("capture", true)  // Also store rendered content in ScalarStore
    .build_hashmap();

pipeline
    .add_namespace(NamespaceBuilder::new("render"))
    .await?
    .add_command::<TemplateCommand>("page", &template_attrs)
    .await?;
}

Inline Template Substitution

Use ctx.substitute() for inline template rendering:

#![allow(unused)]
fn main() {
// In command execution
let greeting = ctx.substitute("Hello, {{ user.name }}!").await?;
}

Condition Expressions

The ConditionCommand uses Tera expressions for branching:

#![allow(unused)]
fn main() {
let condition_attrs = ObjectBuilder::new()
    .insert("branches", ScalarValue::Array(vec![
        ObjectBuilder::new()
            .insert("name", "is_us")
            .insert("if", "region is starting_with(\"us-\")")
            .insert("then", "Region {{ region }} is in the US")
            .build_scalar(),
        ObjectBuilder::new()
            .insert("name", "is_eu")
            .insert("if", "region is starting_with(\"eu-\")")
            .insert("then", "Region {{ region }} is in the EU")
            .build_scalar(),
    ]))
    .insert("default", "Region {{ region }} is in an unknown area")
    .build_hashmap();
}

Tera Tests

Tera "tests" check conditions on values (used with is keyword):

{% if value is defined %}...{% endif %}
{% if value is undefined %}...{% endif %}
{% if value is odd %}...{% endif %}
{% if value is even %}...{% endif %}
{% if text is containing("needle") %}...{% endif %}
{% if text is starting_with("prefix") %}...{% endif %}
{% if text is ending_with("suffix") %}...{% endif %}
{% if text is matching("regex") %}...{% endif %}

Data Flow Diagram

Template Rendering Flow:
========================

+-------------------+
|   ScalarStore     |
|                   |
| inputs.site_name  |
| inputs.page_title |
| inputs.nav_items  |
| config.debug      |
+--------+----------+
         |
         v
+-------------------+
|   Tera Context    |  <- ScalarStore becomes the template context
+--------+----------+
         |
         v
+-------------------+
|  Template File    |
|                   |
| {{ inputs.xxx }}  |
| {% for item %}    |
| {% if config %}   |
+--------+----------+
         |
         v
+-------------------+
|  Rendered Output  |
|                   |
| <h1>My Site</h1>  |
| <p>Welcome!</p>   |
+-------------------+

Common Patterns

Conditional CSS Classes

<div class="alert {% if level == "error" %}alert-danger{% elif level == "warning" %}alert-warning{% else %}alert-info{% endif %}">
    {{ message }}
</div>

Building URLs with Parameters

<a href="/users/{{ user.id }}?tab={{ tab | default(value="overview") }}">
    View Profile
</a>

JSON Data Embedding

<script>
    const config = {{ config | json_encode(pretty=true) | safe }};
</script>

Iteration with Separators

{% for tag in tags %}{{ tag }}{% if not loop.last %}, {% endif %}{% endfor %}

Troubleshooting

Common Errors

ErrorCauseSolution
Variable not foundPath doesn't exist in ScalarStoreCheck StorePath and ensure data is stored before template renders
Cannot iterate overValue is not an arrayVerify the value type with {% if items is iterable %}
Filter not foundTypo in filter nameCheck Tera documentation for correct filter names

Debugging Tips

  1. Check available data: Use {{ __tera_context }} to dump all available variables (if enabled)

  2. Use default values: Prevent errors from missing data

    {{ maybe_missing | default(value="fallback") }}
    
  3. Test variable existence:

    {% if my_var is defined %}
        {{ my_var }}
    {% else %}
        Variable not set
    {% endif %}
    

Next Steps

Continue to Polars DataFrames to learn about working with tabular data.