Alpha — the platform is in development. Docs cover what ships today; anything unbuilt is marked. Reference
-rw-rw-r-- content:content /docs/reference/filters.dtmpl ● verified against v2.0

Filters

Filters follow a path, separated by spaces. One colon opens a filter's argument list; commas separate the arguments inside it:

{var:title uppercase}
{var:body truncate:120}
{var:price currency:`EUR`,`de_DE`}
{var:title lowercase capitalize}

A second colon is a syntax error naming the corrected form — it is not a second argument separator, and never was.

They apply left to right, so the last one wins the final shape.

Below, filter:a,b means "takes arguments a then b". Every argument is optional unless stated; the defaults are in the table.

The one that catches everybody#

default and coalesce look interchangeable. They are not.

{var:count default:`none`}
{var:count coalesce:`none`}

With count = 0 the first prints none and the second prints 0.

default falls back on any falsy value, which includes 0, "0", "", false and empty arrays. coalesce falls back only on null, "" and false, so a real zero survives.

Reach for coalesce on anything numeric. Use default for strings where empty and missing should be treated the same.

Note the neighbour: {var:count default=none} — with an = rather than a : — is a different thing. That one is the path fallback and applies only when the path is missing, not when it is falsy.

Text#

Filter Effect
uppercase mb_strtoupper
lowercase mb_strtolower
capitalize first letter upper
title Every Word Capitalised
truncate:len,suffix cut to length, default 100, suffix ...
truncate_words:n,suffix cut to a whole number of words, default 30
slug URL-friendly lowercase slug
pad:len,char,side left, right (default) or both
repeat:n repeat the string
wrap:width,break,cut word wrap, default width 75
replace:from,to substring replace; the piped value is the subject
split:sep,limit string to array, default separator ,
escape / e encode explicitly — rarely needed, output is encoded anyway
raw emit as markup — see below

Numbers#

Filter Effect
currency:code,locale localised money, defaults USD / en_US
percentage:decimals multiplies by 100 and appends %
round:precision floor ceil abs as named
add:n sub:n mul:n div:n mod:n arithmetic
clamp:min,max bound a value, defaults 0 / 100
filesize:precision bytes to a human-readable size

Dates#

Filter Effect
date:format defaults Y-m-d H:i:s
relative_time "3 hours ago"

Collections#

Filter Effect
count / length element count; length also handles strings
first / last ends of an array
join:sep implode, default ,
keys / values array parts
reverse sort unique flatten as named
slice:offset,length sub-array, keys preserved
map:filter apply a named filter to every element
filter_by:key,value keep elements whose key equals value

Logic#

Filter Effect
default:fallback fallback on any falsy value
coalesce:fallback fallback only on null, "" or false
yesno:yes,no boolean to words, defaults Yes / No
ternary:then,else inline choice

Serialisation#

Filter Effect
json pretty-printed, unescaped unicode
url_encode / url_decode as named

Safe PHP functions#

A fixed allow-list of 41 pure value-transform functions is reachable behind a php. prefix:

{var:name php.strtoupper}
{var:x php.str_pad:8,`0`,0}

The list is compiled into the engine. It is:

strtoupper strtolower ucfirst ucwords trim ltrim rtrim strlen str_repeat wordwrap nl2br strip_tags htmlspecialchars htmlentities urlencode rawurlencode urldecode substr mb_substr mb_strtoupper mb_strtolower str_replace str_pad strrev number_format round ceil floor abs max min count implode array_reverse array_unique array_keys array_values date strtotime json_encode base64_encode

Anything not on it raises a SecurityException rather than being called — a template can never reach an arbitrary PHP function. The list is not configurable: it cannot be extended or narrowed by a host, and the php. prefix is reserved, so a host cannot register a filter into it either. The prefix exists so the boundary is visible in the template.

raw#

{var:body raw}

Output is encoded by default (see language.md), and this is the per-value way out. It is a filter rather than a setting so it appears at the point of use: a reviewer can find every value that bypasses encoding by searching for one word.

Marking is sticky — once a value is raw, the rest of the chain stays raw:

{var:body escape php.nl2br}

encodes the body once, then keeps the <br /> tags nl2br adds, instead of encoding them a second time.

Adding your own#

$engine->registerFilter('initials', static fn (string $v): string
    => implode('', array_map(static fn ($p) => mb_substr($p, 0, 1), explode(' ', $v))));
{var:user.fullName initials}

$engine->getFilters()->register(...) is the same thing one level down, if you already hold the registry.

A filter is any callable taking the value first and its arguments after. Keep them pure: they run once per value per render, and a filter that touches the database turns one page into N queries.

A filter's return value is encoded like any other, so a filter that builds markup must return a CoolMS\Dtmpl\Runtime\RenderedHtml to say so:

$engine->registerFilter('badge', static fn (string $v)
    => new CoolMS\Dtmpl\Runtime\RenderedHtml('<span class="badge">' . htmlspecialchars($v) . '</span>'));

Names beginning php. are rejected — that prefix belongs to the allow-list above.