Forma Template Engine

A lightweight, Handlebars-inspired template engine for building dynamic HTML pages.

Overview

Forma is Strada's built-in template rendering library. It supports variable substitution, loops, conditionals, nested object access, and layout composition. Forma is used by the Cannoli web framework for rendering dynamic pages.

Key Features

Getting Started

use lib "lib";
use Forma;

# Initialize with template directory
Forma::init("./templates");

# Create template data
my hash %vars = ();
$vars{"title"} = "Welcome";
$vars{"username"} = "Alice";

# Render a template file
my str $html = Forma::render("page.html", \%vars);
say($html);

Template Syntax

Variables

Use double curly braces to output variables:

<!-- Template -->
<h1>Hello, {{username}}!</h1>
<p>You have {{message_count}} unread messages.</p>

Nested Object Access

Access nested properties using dot notation:

<!-- Template -->
<div class="profile">
    <h2>{{user.name}}</h2>
    <p>{{user.address.city}}, {{user.address.country}}</p>
    <p>Email: {{user.contact.email}}</p>
</div>
# Strada code
my hash %vars = ();
$vars{"user"} = {
    "name" => "Alice",
    "address" => {
        "city" => "Portland",
        "country" => "USA"
    },
    "contact" => {
        "email" => "alice@example.com"
    }
};

Loops with {{#each}}

Loop over arrays with the {{#each}} block. Hash fields are accessible directly:

<!-- Template -->
<ul>
{{#each users}}
    <li>{{name}} - {{email}}</li>
{{/each}}
</ul>

Use a named variable for clearer templates:

<!-- Template -->
<ul>
{{#each user in users}}
    <li>{{user.name}} - {{user.email}}</li>
{{/each}}
</ul>

Loop Metadata

Access loop metadata with special @ variables:

<!-- Template -->
<table>
{{#each item in items}}
    <tr class="{{#if @first}}first-row{{/if}} {{#if @last}}last-row{{/if}}">
        <td>{{@index}}</td>
        <td>{{item.name}}</td>
    </tr>
{{/each}}
</table>
VariableDescription
{{@index}}Current iteration index (0-based)
{{@first}}True (1) on first iteration
{{@last}}True (1) on last iteration

Conditionals with {{#if}}

Show or hide content based on a condition:

<!-- Template -->
{{#if logged_in}}
    <p>Welcome back, {{username}}!</p>
    <a href="/logout">Logout</a>
{{else}}
    <p>Please log in to continue.</p>
    <a href="/login">Login</a>
{{/if}}

Truthiness

Values are considered "truthy" if they are:

Inverse Conditionals with {{#unless}}

Show content when a condition is false:

<!-- Template -->
{{#unless has_items}}
    <p>Your cart is empty.</p>
{{/unless}}

{{#unless logged_in}}
    <a href="/login">Login</a>
{{/unless}}

Context Switching with {{#with}}

Change the current scope to a nested object:

<!-- Template -->
{{#with user}}
    <div class="user-card">
        <h3>{{name}}</h3>
        <p>{{email}}</p>
        {{#with address}}
            <p>{{city}}, {{country}}</p>
        {{/with}}
    </div>
{{/with}}

Variable Assignment with {{#set}}

Create or assign variables within templates:

<!-- Template -->
{{#set page_title = "Dashboard"}}
{{#set full_name = user.first_name}}

<title>{{page_title}}</title>
<h1>Welcome, {{full_name}}!</h1>

Range Loops with {{#range}}

Iterate over a numeric range:

<!-- Generate page numbers -->
{{#range 1 5}}
    <a href="/page/{{.}}">Page {{.}}</a>
{{/range}}

<!-- Output: Page 1, Page 2, Page 3, Page 4, Page 5 -->

Range loops support the same metadata variables as {{#each}}:

{{#range 1 3}}
    <span{{#if @first}} class="first"{{/if}}>{{.}}</span>
{{/range}}

Comparison Helpers

Use comparison helpers for more complex conditionals:

<!-- Equal -->
{{#if_eq status "active"}}
    <span class="badge-green">Active</span>
{{else}}
    <span class="badge-gray">Inactive</span>
{{/if_eq}}

<!-- Not equal -->
{{#if_ne role "admin"}}
    <p>Regular user</p>
{{/if_ne}}

<!-- Numeric comparisons -->
{{#if_gt score 80}}<span>Excellent!</span>{{/if_gt}}
{{#if_lt items_count 5}}<span>Low stock</span>{{/if_lt}}
{{#if_ge age 18}}<span>Adult</span>{{/if_ge}}
{{#if_le price 100}}<span>Budget-friendly</span>{{/if_le}}
HelperComparison
{{#if_eq var "value"}}Equal (string comparison)
{{#if_ne var "value"}}Not equal
{{#if_gt var value}}Greater than (numeric)
{{#if_lt var value}}Less than (numeric)
{{#if_ge var value}}Greater than or equal
{{#if_le var value}}Less than or equal

String Helpers

Transform strings with built-in helpers:

<!-- Convert to uppercase -->
{{upper name}}          <!-- "alice" -> "ALICE" -->

<!-- Convert to lowercase -->
{{lower title}}         <!-- "HELLO" -> "hello" -->

<!-- Capitalize first letter -->
{{capitalize name}}     <!-- "alice" -> "Alice" -->

<!-- Truncate with ellipsis -->
{{truncate description 50}}   <!-- Long text... -->

Array Helpers

Work with arrays in templates:

<!-- Get array/string length -->
You have {{length items}} items

<!-- Get first/last element -->
First: {{first items}}
Last: {{last items}}

<!-- Join array elements -->
Tags: {{join tags ", "}}   <!-- "apple, banana, cherry" -->

Number Formatting

Format numbers for display:

<!-- Add thousands separators -->
Price: ${{commas price}}    <!-- 1234567 -> "1,234,567" -->

<!-- Format with decimal places -->
{{format_number pi 2}}      <!-- 3.14159 -> "3.14" -->

<!-- Pluralize based on count -->
{{plural count "item" "items"}}   <!-- "1 item" or "5 items" -->

Pipe Syntax with Filters

Use pipe syntax to apply filters to values:

<!-- Default value for missing variables -->
{{username | default "Guest"}}

<!-- Transform with filters -->
{{name | upper}}
{{email | lower}}
{{title | capitalize}}
{{user_content | escape}}
FilterDescription
default "value"Use value if variable is empty/missing
upperConvert to uppercase
lowerConvert to lowercase
capitalizeCapitalize first letter
escapeHTML-escape the value

JSON Output

Output a variable as JSON (useful for JavaScript integration):

<script>
    const userData = {{json user}};
    const items = {{json items}};
</script>

Comments

Add comments that won't appear in the output:

{{!-- This comment will not be rendered --}}
<div>
    {{!-- TODO: Add user avatar here --}}
    <h2>{{username}}</h2>
</div>

Debugging with {{dump}}

Output a variable's contents for debugging:

<!-- Template -->
<pre>{{dump user}}</pre>

API Reference

Forma::init($dir)

Initialize the template system with a directory path.

Forma::init("./templates");

Forma::render($name, $vars)

Render a template file with variables.

my str $html = Forma::render("page.html", \%vars);

Forma::render_string($template, $vars)

Render a template string directly.

my str $html = Forma::render_string("Hello, {{name}}!", \%vars);

Forma::render_safe($name, $vars)

Render with automatic HTML escaping for all variables. Use {{{var}}} for unescaped output.

# User input is automatically escaped
my str $html = Forma::render_safe("comment.html", \%vars);

Forma::render_with_layout($template, $layout, $vars)

Render a template within a layout. The layout should contain {{content}}.

# layout.html contains: <html><body>{{content}}</body></html>
my str $html = Forma::render_with_layout("page.html", "layout.html", \%vars);

Forma::include($name, $vars)

Include and render a partial template.

my str $sidebar = Forma::include("sidebar.html", \%vars);

Forma::set_cache($enabled)

Enable or disable template caching.

Forma::set_cache(0);  # Disable for development
Forma::set_cache(1);  # Enable for production (default)

Forma::clear_cache()

Clear the template cache.

Forma::clear_cache();

Forma::escape_html($string)

Escape HTML special characters in a string.

my str $safe = Forma::escape_html("<script>alert('xss')</script>");
# Returns: &lt;script&gt;alert('xss')&lt;/script&gt;

Custom Helpers (Functional Macros)

Register your own helper functions that can be called from templates. This allows you to extend Forma with domain-specific functionality.

Forma::register_helper($name, $handler)

Register a custom helper function.

# Define a helper function
# Arguments: $args (array ref), $vars (template variables)
# Returns: string result
func double_helper(scalar $args, scalar $vars) str {
    my array @a = @{$args};
    if (scalar(@a) == 0) {
        return "0";
    }
    my int $n = @a[0] + 0;
    return "" . ($n * 2);
}

# Register it
Forma::register_helper("double", \&double_helper);

# Use in templates
# {{double count}}  -> doubles the value of count
# {{double 21}}     -> outputs "42"

Using Custom Helpers in Templates

Custom helpers can be used in two ways:

<!-- As a direct helper -->
{{double count}}
{{greet name "Hello"}}

<!-- As a pipe filter -->
{{count | double}}
{{name | wrap "strong"}}

Example: Custom Helpers

# Greeting helper with optional salutation
func greet_helper(scalar $args, scalar $vars) str {
    my array @a = @{$args};
    my str $name = scalar(@a) > 0 ? "" . @a[0] : "World";
    my str $greeting = scalar(@a) > 1 ? "" . @a[1] : "Hello";
    return $greeting . ", " . $name . "!";
}

Forma::register_helper("greet", \&greet_helper);

# Template usage:
# {{greet name}}         -> "Hello, Alice!"
# {{greet name "Hi"}}    -> "Hi, Alice!"
# {{greet "Bob" "Hey"}}  -> "Hey, Bob!"

Example: Custom Filter for Pipe Syntax

# Wrap text in HTML tags
func wrap_filter(scalar $args, scalar $vars) str {
    my array @a = @{$args};
    my str $val = scalar(@a) > 0 ? "" . @a[0] : "";
    my str $tag = scalar(@a) > 1 ? "" . @a[1] : "span";
    return "<" . $tag . ">" . $val . "</" . $tag . ">";
}

Forma::register_helper("wrap", \&wrap_filter);

# Template usage with pipe syntax:
# {{name | wrap "strong"}}  -> "<strong>Alice</strong>"
# {{title | wrap "h1"}}     -> "<h1>Page Title</h1>"

Forma::unregister_helper($name)

Remove a registered helper.

Forma::unregister_helper("double");

Forma::has_helper($name)

Check if a helper is registered.

if (Forma::has_helper("greet")) {
    say("greet helper is available");
}

Forma::list_helpers()

Get all registered helper names.

my array @helpers = Forma::list_helpers();
say("Registered: " . join(", ", @helpers));

Forma::clear_helpers()

Remove all custom helpers.

Forma::clear_helpers();

Example: Complete Web Page

layout.html

<!DOCTYPE html>
<html>
<head>
    <title>{{title}} - My Site</title>
</head>
<body>
    <nav>
        {{#if logged_in}}
            <span>Welcome, {{username}}</span>
            <a href="/logout">Logout</a>
        {{else}}
            <a href="/login">Login</a>
        {{/if}}
    </nav>
    <main>
        {{content}}
    </main>
</body>
</html>

products.html

<h1>Products</h1>

{{#if has_products}}
    <div class="product-grid">
    {{#each product in products}}
        <div class="product {{#if @first}}featured{{/if}}">
            <h2>{{product.name}}</h2>
            <p class="price">${{product.price}}</p>
            {{#if product.in_stock}}
                <button>Add to Cart</button>
            {{else}}
                <span class="out-of-stock">Out of Stock</span>
            {{/if}}
        </div>
    {{/each}}
    </div>
{{else}}
    <p>No products available.</p>
{{/if}}

Strada Code

use lib "lib";
use Forma;

Forma::init("./templates");

my hash %vars = ();
$vars{"title"} = "Products";
$vars{"logged_in"} = 1;
$vars{"username"} = "Alice";
$vars{"products"} = [
    { "name" => "Widget", "price" => "19.99", "in_stock" => 1 },
    { "name" => "Gadget", "price" => "29.99", "in_stock" => 0 },
    { "name" => "Gizmo", "price" => "39.99", "in_stock" => 1 }
];
$vars{"has_products"} = 1;

my str $html = Forma::render_with_layout("products.html", "layout.html", \%vars);
say($html);

Using with Cannoli

Forma integrates seamlessly with the Cannoli web framework:

use lib "lib";
use cannoli;
use Forma;

Forma::init("./templates");

func handle_home(scalar $c) hash {
    my hash %vars = ();
    $vars{"title"} = "Home";
    $vars{"message"} = "Welcome to my site!";

    my str $html = Forma::render_with_layout("home.html", "layout.html", \%vars);

    $c->status(200);
    $c->content_type("text/html");
    $c->write_body($html);
    return $c->build_response();
}

Additional Built-in Helpers

Math Helpers

Perform arithmetic operations in templates:

<!-- Basic arithmetic -->
{{add count 10}}           <!-- count + 10 -->
{{subtract total 5}}       <!-- total - 5 -->
{{multiply price quantity}}<!-- price * quantity -->
{{divide total count}}     <!-- total / count -->
{{mod index 2}}            <!-- index % 2 (modulo) -->

<!-- Rounding -->
{{round price}}            <!-- Round to nearest integer -->
{{round price 2}}          <!-- Round to 2 decimal places -->

Additional String Helpers

More string manipulation tools:

<!-- Replace text -->
{{replace text "old" "new"}}     <!-- Replace all occurrences -->

<!-- String checks -->
{{#if (contains name "admin")}}
    <span class="admin-badge">Admin</span>
{{/if}}
{{#if (starts_with filename "temp_")}}...{{/if}}
{{#if (ends_with filename ".txt")}}...{{/if}}

<!-- Padding -->
{{pad_left id 5 "0"}}      <!-- "42" -> "00042" -->
{{pad_right name 10 "."}}  <!-- "Bob" -> "Bob......." -->

<!-- Repeat string -->
{{repeat "=" 20}}          <!-- "====================" -->
{{repeat "*" count}}       <!-- Repeat count times -->

<!-- Strip HTML tags -->
{{strip_tags html_content}}<!-- Remove all HTML tags -->

URL Encoding

Encode and decode URL components:

<!-- Encode for use in URLs -->
<a href="/search?q={{url_encode query}}">Search</a>

<!-- Decode URL-encoded strings -->
{{url_decode encoded_text}}

Date and Time Helpers

Format dates and display relative time:

<!-- Format timestamps -->
{{date_format timestamp "%Y-%m-%d"}}        <!-- 2026-01-27 -->
{{date_format timestamp "%B %d, %Y"}}       <!-- January 27, 2026 -->
{{date_format timestamp "%H:%M:%S"}}        <!-- 14:30:45 -->
{{date_format timestamp "%A"}}              <!-- Tuesday -->

<!-- Relative time (time ago) -->
{{time_ago created_at}}    <!-- "5 minutes ago", "2 hours ago", "3 days ago" -->
Format CodeDescriptionExample
%Y4-digit year2026
%mMonth (01-12)01
%dDay (01-31)27
%HHour (00-23)14
%MMinute (00-59)30
%SSecond (00-59)45
%BFull month nameJanuary
%bAbbreviated monthJan
%AFull weekday nameTuesday
%aAbbreviated weekdayTue

Currency and Percentage Formatting

<!-- Currency formatting -->
{{currency price}}         <!-- "$1,234.56" (default USD) -->
{{currency price "EUR"}}   <!-- "€1,234.56" -->
{{currency price "GBP"}}   <!-- "£1,234.56" -->
{{currency price "JPY"}}   <!-- "¥1,235" (no decimals) -->

<!-- Percentage formatting -->
{{percent ratio}}          <!-- 0.75 -> "75%" -->
{{percent ratio 1}}        <!-- 0.756 -> "75.6%" -->

Inline Conditional (iif)

Ternary-style conditionals in a single expression:

<!-- value if true : value if false -->
{{iif is_active "Active" "Inactive"}}
{{iif count "Has items" "Empty"}}

<!-- Use in attributes -->
<button class="btn {{iif primary 'btn-primary' 'btn-secondary'}}">
    {{label}}
</button>

Complete Helper Reference

CategoryHelperUsage
Stringupper{{upper name}}
lower{{lower name}}
capitalize{{capitalize name}}
truncate{{truncate text 50}}
String (adv)replace{{replace text "a" "b"}}
contains{{contains str "sub"}}
starts_with{{starts_with str "pre"}}
ends_with{{ends_with str "suf"}}
pad_left{{pad_left n 5 "0"}}
pad_right{{pad_right s 10 "."}}
Arraylength{{length items}}
first{{first items}}
last{{last items}}
join{{join items ", "}}
Mathadd{{add a b}}
subtract{{subtract a b}}
multiply{{multiply a b}}
divide{{divide a b}}
mod{{mod a b}}
round{{round n 2}}
Numberscommas{{commas 1234567}}
format_number{{format_number pi 2}}
currency{{currency price "USD"}}
percent{{percent ratio 1}}
Date/Timedate_format{{date_format ts "%Y-%m-%d"}}
time_ago{{time_ago timestamp}}
plural{{plural n "item" "items"}}
URL/HTMLurl_encode{{url_encode query}}
url_decode{{url_decode str}}
strip_tags{{strip_tags html}}
Utilityrepeat{{repeat "=" 20}}
iif{{iif cond "yes" "no"}}
json{{json object}}

Best Practices

Tip: For complex web applications, consider using Pacco (the Strada package registry) as an example of Forma and Cannoli working together.