The UntitledTemplateEngine aims to provide text templates with superpowers. The templating engine uses syntax that is similar to Liquid or Mustache, but while looking alike, they are totally different. That’s because this template engine uses a lisp like language for templates, that allows for plugging of C and C++ functions directly into the parser.

In simple terms, this means that you can create custom logic with the power and performance of C and C++(+ other languages, since creating language bindings for the engine is extremely easy using the C API)

By default, however, the engine provides the most minimal that allows for basic logic, mainly because of bloat and security considerations. Additionally, you can use the APIs to remove even the minimal logic to create logicless templates, or you can use the UntitledTemplateEngineUtils library that contains C functions for common string, int, float, math, and other use cases.

Syntax

Simple string substitution

First, let’s start with substituting strings, the following code substitutes variables into a string:

The {{ at descriptors 0 }} {{ colour }} fox {{ at action "jump" }} over the {{ at descriptors 1 }} dog

In this example, we have provided the engine with the following variables:

  1. descriptors - Array
  2. colour - String
  3. action - Map

Since the language is lisp-like, variables are also functions and if they’re called they just return their value, so the code {{ colour }} simply returns the value of the colour variable provided to the parser.

Additionally, for Arrays, Strings and Maps, we provide the at function that can index into the given type. Here’s the signature of the at function in C++ style:

// For arrays
string at(Array<string> array, string index);

// For maps
string at(Map<string, string> map, string key);

You might have noticed that all types here use string. That’s because there’s only 1 variable type supported, that being string. While it may be convenient to use a strong typing system, it would make the parser way more complicated and rigid for a minimal library like this. Additionally, it wouldn’t be of great benefit, since most I/O operations are done to text files.

Iterating arrays, maps, and ranges

Array

To iterate an array, simply call the for function like this to create a foreach loop:

{{ for it arr {{ func 
    This is {{ it }}
}} }}

The first argument is the iterator, the second is the array that’s going to be iterated trough and the third argument is a call to the func function, that acts like the body of the for loop.

The func function simply defines a templated text block, where the arguments are simply a variable arguments list. This function will be called and substituted for every element of the array.


To iterate an array using an index, simply use the double iterator for loop like this:

{{ for key val arr {{ func
    Key: {{ key }}
    Value: {{ val }}
}} }}

Range

To iterate a range, simply use the ranged for loop:

{{ for i 0 100 {{ func
    {{ i }}
}} }}

Map

To iterate a map, use the double iterator for loop:

{{ for key val action {{ func
    Key: {{ key }}
    Value: {{ val }}
}} }}

Conditionals

There are 3 types of conditionals, if, switch and cond.

Built-in boolean functions

Before we begin with conditionals, we first need ways to do boolean calculations. Fortunately, the ==, !=, !, && and || functions are built-in, so you don’t have to worry about creating a custom implementation for them

All the functions, except ! accept a variable list of arguments for easily comparing between multiple values on 1 condition. This allows for the following syntax:

{{ || 0 0 0 0 0 0 0 0 0 1 }}

or

{{ == {{ a }} {{ b }} {{ c }} text }}

The only exception is !, because that operator is only unary, since it flips the boolean value.

Boolean literals

Additionally, we provide also provide the standard true and false values as functions, if you don’t want to use 1 and 0. Simply call the functions normally like this: {{ true }} {{ false }}

If

If statements here don’t have else if statements, so they should be used for binary checks. Here is an example of an if statement:

{{ if {{ == {{ value }} "test" }}
    {{ func
        {{ test_val}}
    }}
    {{ func
        {{ not_test_val }}
    }}
}}

Let’s translate this to C++ for better readability

if (value == "test")
    test_val();
else
    not_test_val();

Switch

A switch statement is more suitable if checking for multiple values of a given variable, it looks like this:

{{ switch {{ value }}
    "test" {{ func
        {{ test_val}}
    }}
    "example" {{ func
        {{ example_val }}
    }}
    default {{ func
        {{ fallback_val }}
    }}
}}

The first argument should always be a primitive variable, otherwise the function will terminate with an error.

After the first argument, we have a variable arguments list, where every argument should be a pair of value and function. In an edge case where 2 values don’t have a function between them, the first variable is considered to reference an empty function and therefore nothing will be executed.

The default key is a reserved keyword when used in a switch statement!

In C, the same code looks like this:

switch (value)
{
    case "test":
        test_val();
        break;
    case "test":
        example_val();
        break;
    default:
        fallback_val();
        break;
}

Cond

Cond statements allow representing an if chain, similarly to what’s possible in C/C++ and related languages. Here’s an example:

{{ cond
    {{ == {{ value }} "test" }} {{ func
        {{ test_val }}
    }}
    {{ == {{ value }} "example" }} {{ func
        {{ example_val }}
    }}
    default {{ func
        {{ fallback_val }}
    }}
}}

This syntax is similar to the switch syntax, but here values are the result of boolean expressions. This code translates to the following C++ code:

if (value == "test")
    test_val();
else if (value == "example")
    example_val();
else
    fallback_val();

The default keyword is reserved to mark the fallback statement(else in C).

In an edge case where there are 2 boolean expressions with no function between them, the first boolean expression will be marked as referencing an empty function and therefore nothing will be executed.

Formatting

Escaping templates

If you want a portion of the string to contain templates, but you don’t want them to execute(i.e. a document showcasing templating examples), you can simply put your text between the {{ raw }} and {{ endraw }} functions like this:

{{ raw }}
    This is how you iterate an array in the UntitledTemplateEngine
    {{ for a arr
        {{ func
            {{ a }}
        }}
    }}
{{ endraw }}

Comments

If you want to add comments to your logic, simply use the comment function like this:

{{ comment This is a comment and will be automatically removed
    It can be multiline as well!
}}