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
| Error | Cause | Solution |
|---|---|---|
Variable not found | Path doesn't exist in ScalarStore | Check StorePath and ensure data is stored before template renders |
Cannot iterate over | Value is not an array | Verify the value type with {% if items is iterable %} |
Filter not found | Typo in filter name | Check Tera documentation for correct filter names |
Debugging Tips
-
Check available data: Use
{{ __tera_context }}to dump all available variables (if enabled) -
Use default values: Prevent errors from missing data
{{ maybe_missing | default(value="fallback") }} -
Test variable existence:
{% if my_var is defined %} {{ my_var }} {% else %} Variable not set {% endif %}
Reference Links
Next Steps
Continue to Polars DataFrames to learn about working with tabular data.