Templating

standout-render uses a two-pass templating system that combines a template engine for logic and data binding with a custom BBCode-like syntax for styling. This separation keeps templates readable while providing full control over both content and presentation.

The default engine is MiniJinja (Jinja2-compatible), but alternative engines are available. See Template Engines for options including a lightweight SimpleEngine for reduced binary size.


Two-Pass Rendering Pipeline

Templates are processed in two distinct passes:

Template + Data → [Pass 1: MiniJinja] → Text with style tags → [Pass 2: BBParser] → Final output

Pass 1 - MiniJinja: Standard template processing. Variables are substituted, control flow executes, filters apply.

Pass 2 - BBParser: Style tag processing. Bracket-notation tags are converted to ANSI escape codes (or stripped, depending on output mode).

Example

Template:     [title]{{ name }}[/title] has {{ count }} items
Data:         { name: "Report", count: 42 }

After Pass 1: [title]Report[/title] has 42 items
After Pass 2: \x1b[1;36mReport\x1b[0m has 42 items  (or plain: "Report has 42 items")

This separation means:

  • Template logic (loops, conditionals) is handled by MiniJinja—a mature, well-documented engine
  • Style application is a simple, predictable transformation
  • You can debug each pass independently

MiniJinja Basics

MiniJinja implements Jinja2 syntax, a widely-used templating language. Here's a quick overview:

Variables

{{ variable }}
{{ object.field }}
{{ list[0] }}

Control Flow

{% if condition %}
  Show this
{% elif other_condition %}
  Show that
{% else %}
  Default
{% endif %}

{% for item in items %}
  {{ loop.index }}. {{ item.name }}
{% endfor %}

Filters

{{ name | upper }}
{{ list | length }}
{{ value | default("N/A") }}
{{ text | truncate(20) }}

Comments

{# This is a comment and won't appear in output #}

For comprehensive MiniJinja documentation, see the MiniJinja documentation.


Style Tags

Style tags use BBCode-like bracket notation to apply named styles from your theme:

[style-name]content to style[/style-name]

Basic Usage

[title]Report Summary[/title]
[error]Something went wrong![/error]
[muted]Last updated: {{ timestamp }}[/muted]

Nesting

Tags can nest properly:

[outer][inner]nested content[/inner][/outer]

Spanning Lines

Tags can span multiple lines:

[panel]
This is a multi-line
block of styled content
[/panel]

With Template Logic

Style tags and MiniJinja work together seamlessly:

[title]{% if custom_title %}{{ custom_title }}{% else %}Default Title{% endif %}[/title]

{% for task in tasks %}
[{{ task.status }}]{{ task.title }}[/{{ task.status }}]
{% endfor %}

The second example shows dynamic style names—the style applied depends on the value of task.status.


Processing Modes

Pass 2 (BBParser) processes style tags differently based on the output mode:

ModeBehaviorUse Case
TermReplace tags with ANSI escape codesRich terminal output
TextStrip tags completelyPlain text, pipes, files
TermDebugKeep tags as literal textDebugging, testing

Example

Template: [title]Hello[/title]

  • Term: \x1b[1;36mHello\x1b[0m (rendered as cyan bold)
  • Text: Hello
  • TermDebug: [title]Hello[/title]

Setting the Mode

#![allow(unused)]
fn main() {
use standout_render::{render_with_output, OutputMode};

// Rich terminal
let output = render_with_output(template, &data, &theme, OutputMode::Term)?;

// Plain text
let output = render_with_output(template, &data, &theme, OutputMode::Text)?;

// Debug (tags visible)
let output = render_with_output(template, &data, &theme, OutputMode::TermDebug)?;

// Auto-detect based on TTY
let output = render_with_output(template, &data, &theme, OutputMode::Auto)?;
}

Auto Mode

OutputMode::Auto detects the appropriate mode:

  • If stdout is a TTY with color support → Term
  • If stdout is a pipe or redirect → Text

For standout framework users: The framework's --output CLI flag automatically sets the output mode. See standout documentation for details.


Built-in Filters

Beyond MiniJinja's standard filters, standout-render provides formatting filters:

Column Formatting

{{ value | col(10) }}                              {# pad/truncate to 10 chars #}
{{ value | col(20, align="right") }}               {# right-align in 20 chars #}
{{ value | col(15, truncate="middle") }}           {# truncate in middle #}
{{ value | col(15, truncate="start", ellipsis="...") }}

Padding

{{ "42" | pad_left(8) }}      {# "      42" #}
{{ "hi" | pad_right(8) }}     {# "hi      " #}
{{ "hi" | pad_center(8) }}    {# "   hi   " #}

Truncation

{{ long_text | truncate_at(20) }}                   {# "Very long text th..." #}
{{ path | truncate_at(30, "middle", "...") }}      {# "/home/.../file.txt" #}
{{ text | truncate_at(20, "start") }}              {# "...end of the text" #}

Display Width

{% if value | display_width > 20 %}
  {{ value | truncate_at(20) }}
{% else %}
  {{ value }}
{% endif %}

Returns visual width (handles Unicode—CJK characters count as 2).

Style Application

{{ value | style_as("error") }}                    {# wraps in [error]...[/error] #}
{{ task.status | style_as(task.status) }}         {# dynamic: [pending]pending[/pending] #}

Template Registry

When using the Renderer struct, templates are resolved by name through a registry:

#![allow(unused)]
fn main() {
use standout_render::Renderer;

let mut renderer = Renderer::new(theme)?;

// Add inline template
renderer.add_template("greeting", "Hello, [name]{{ name }}[/name]!")?;

// Add directory of templates
renderer.add_template_dir("./templates")?;

// Render by name
let output = renderer.render("greeting", &data)?;
}

Resolution Priority

  1. Inline templates (added via add_template())
  2. Directory templates (from add_template_dir())

File Extensions

Supported extensions (in priority order): .jinja, .jinja2, .j2, .stpl, .txt

When you request "report", the registry checks:

  • Inline template named "report"
  • report.jinja in registered directories
  • report.jinja2, report.j2, report.stpl, report.txt (lower priority)

The .stpl extension is for SimpleEngine templates. See Template Engines for details.

Template Names

Template names are derived from relative paths:

templates/
├── greeting.jinja       → "greeting"
├── reports/
│   └── summary.jinja    → "reports/summary"
└── errors/
    └── 404.jinja        → "errors/404"

Including Templates

Templates can include other templates using MiniJinja's include syntax:

{# main.jinja #}
[title]{{ title }}[/title]

{% include "partials/header.jinja" %}

{% for item in items %}
  {% include "partials/item.jinja" %}
{% endfor %}

{% include "partials/footer.jinja" %}

This enables reusable components across your application.


Context Variables

Beyond your data, you can inject additional context into templates:

#![allow(unused)]
fn main() {
use standout_render::{render_with_vars, OutputMode};
use std::collections::HashMap;

let mut vars = HashMap::new();
vars.insert("version", "1.0.0");
vars.insert("app_name", "MyApp");

let output = render_with_vars(
    "{{ app_name }} v{{ version }}: {{ message }}",
    &data,
    &theme,
    OutputMode::Term,
    vars,
)?;
}

When handler data and context variables have the same key, handler data wins. Context is supplementary.


Structured Output

For machine-readable output (JSON, YAML, CSV), templates are bypassed entirely:

#![allow(unused)]
fn main() {
use standout_render::{render_auto, OutputMode};

// Template is used for Term/Text modes
// Data is serialized directly for Json/Yaml/Csv
let output = render_auto(template, &data, &theme, OutputMode::Json)?;
}
ModeBehavior
TermRender template, apply styles
TextRender template, strip styles
TermDebugRender template, keep style tags
Jsonserde_json::to_string_pretty(data)
Yamlserde_yaml::to_string(data)
CsvFlatten and format as CSV

This means your serializable data types automatically support structured output without additional code.


Validation

Check templates for unknown style tags before deploying:

#![allow(unused)]
fn main() {
use standout_render::validate_template;

let errors = validate_template(template, &sample_data, &theme);
if !errors.is_empty() {
    for error in &errors {
        eprintln!("Unknown style tag: [{}]", error.tag_name);
    }
}
}

Validation catches:

  • Misspelled style names
  • References to undefined styles
  • Mismatched opening/closing tags

API Reference

Render Functions

#![allow(unused)]
fn main() {
use standout_render::{
    render,                  // Basic: template + data + theme
    render_with_output,      // With explicit output mode
    render_with_mode,        // With output mode + color mode
    render_with_vars,        // With extra context variables
    render_auto,             // Auto-dispatch template vs serialize
    render_auto_with_context,
};

// Basic
let output = render(template, &data, &theme)?;

// With output mode
let output = render_with_output(template, &data, &theme, OutputMode::Term)?;

// With color mode override (for testing)
let output = render_with_mode(template, &data, &theme, OutputMode::Term, ColorMode::Dark)?;

// Auto (template for text modes, serialize for structured)
let output = render_auto(template, &data, &theme, OutputMode::Json)?;
}

Renderer Struct

#![allow(unused)]
fn main() {
use standout_render::Renderer;

let mut renderer = Renderer::new(theme)?;
renderer.add_template("name", "content")?;
renderer.add_template_dir("./templates")?;

let output = renderer.render("name", &data)?;
let output = renderer.render_with_mode("name", &data, OutputMode::Text)?;
}