Interpolations
Insert values using double curly braces. Missing keys render as an empty string.
{{ title }}
{{ user.name.first }}
// If a key cannot be resolved it becomes "" (empty)
||, ??) and filters are supported.
Escaping
Escaped output is the default. Raw output is explicit.
{{ value }} // escaped (default)
{{= value }} // raw (no escaping)
Dot-path resolution
Use dot-paths to traverse nested objects.
Hello {{ user.profile.firstName }}!
Fallbacks
Choose between “empty-ish” vs “nullish” fallback behavior. Fallbacks are chainable left-to-right.
{{ user.name || user.email || 'anonymous' }}
{{ v || 'fallback' }} // replaces undefined, null, '', [], {}
{{ v ?? 'fallback' }} // replaces undefined, null only
Filters
Pipe values through filters, left-to-right. Unknown filters are ignored (fail-safe behavior).
{{ name | trim | upper }}
{{ price | number(2, ',', '.') }}
{{ createdAt | dateformat('YYYY-MM-DD') }}
Filter names
In templates, filter names are parsed as \w+. When registering filters, names must match ^[a-z][\w]*$.
Filter arguments
Arguments are optional and comma-separated.
{{ title | replace(' ', '-') | lower }}
{{ price | number(2, ',', '.') }}
Supported argument literals:
- strings in single or double quotes (basic backslash escaping)
- numbers (
123,-1,3.14) - booleans (
true/false, case-insensitive) null(case-insensitive)
Unsupported argument tokens are ignored.
Built-in filters
cryTemplate ships with the following filters:
- upper, lower, trim, replace(from, to), string
- number(decimals?, decimalSep?, thousandsSep?)
- json, urlencode, dateformat(format?)
upper
Uppercases the string representation. Returns '' for null/undefined.
lower
Lowercases the string representation. Returns '' for null/undefined.
trim
Trims whitespace from the string representation. Returns '' for null/undefined. Optional mode: trim('left'), trim('right'), trim('both') (default).
replace(from, to)
Performs a literal, global replacement on the string representation. If from is '', the input is returned unchanged.
string
Forces an early string conversion: null/undefined → '', otherwise String(value).
number(decimals?, decimalSep?, thousandsSep?)
Formats numbers. It tries Number(value) if the value is not already a number.
If the numeric result is not finite, it returns '' for null/undefined and otherwise String(value).
{{ price | number(2) }} // 1234.50
{{ price | number(2, ',') }} // 1234,50
{{ price | number(2, ',', '.') }} // 1.234,50
json
Serializes via JSON.stringify(value). If that returns undefined, the filter returns ''.
urlencode
Encodes via encodeURIComponent(String(value)).
dateformat(format?)
Formats Date, timestamp numbers, or date-like strings. Invalid inputs return ''. Default format: YYYY-MM-DD HH:mm:ss.
{{ createdAt | dateformat('YYYY-MM-DD HH:mm:ss') }}
{{ createdAt | dateformat }}
Supported Day.js-style tokens (subset without Day.js): YYYY, YY, M/MM, D/DD, H/HH, h/hh, m/mm, s/ss, Z, A/a.
Escaping: anything inside [...] is treated as a literal and brackets are removed.
{{ createdAt | dateformat('YYYY-MM-DD [YYYY-MM-DD]') }}
Day.js integration (optional)
cryTemplate does not require Day.js, but you can plug it in for full formatting support.
import dayjs from 'dayjs';
import { setDayjsTemplateReference, renderTemplate } from 'crytemplate';
setDayjsTemplateReference(dayjs);
const out = renderTemplate("{{ d | dateformat('MMM YYYY') }}", { d: new Date() });
Conditionals
{% if user.admin %}
Admin
{% elseif user.moderator %}
Moderator
{% else %}
User
{% endif %}
Truthiness
- Arrays are truthy only when non-empty.
- Objects are truthy only when they have at least one own key.
- Everything else uses normal boolean coercion.
Negation
{% if !user.admin %}not admin{% endif %}
{% if not user.admin %}not admin{% endif %}
Comparisons, logical ops, grouping
{% if status == 'open' %}...{% endif %}
{% if age >= 18 %}adult{% endif %}
{% if (a == 'x' && b) || c %}ok{% endif %}
Precedence is && before ||.
Loops
Iterate arrays (and objects) with {% each ... %}.
{% each items as it %}
- {{ it }}
{% endeach %}
Forms
- Arrays:
{% each listExpr as item %}...{% endeach %} - Arrays with index:
{% each listExpr as item, i %}...{% endeach %} - Objects: iterates own keys and exposes
{ key, value }as the loop variable
{% each items as it, i %}
{{ i }}: {{ it }}
{% endeach %}
{% each user as e %}
{{ e.key }} = {{ e.value }}
{% endeach %}
Scoping
outside={{ it }}
{% each items as it %}
inside={{ it }}
{% endeach %}
outside-again={{ it }}
Comments
Add comments using {# ... #} blocks. Comments are ignored during rendering.
{# This is a comment #}
{#
This is also a comment.
It can span multiple lines.
#}
Whitespace handling
cryTemplate performs a small whitespace normalization around control tags to make “block style” templates behave closer to what users typically expect.
- Trim after control tags: exactly one line break immediately after a correctly parsed control tag closing
%}is removed. - Supported line breaks:
\nand\r\n. - Only one: if multiple line breaks follow, only the first one is removed.
- No other trimming: spaces/tabs after
%}are not removed. - Fail-safe behavior: misplaced/malformed control tokens preserved as literal text do not trigger trimming.
{% if user %}
Hello {{ user.name }}
{% endif %}
Goodbye
In this example, the newline directly after {% if user %} and after {% endif %} is removed, so Hello ... and Goodbye render without extra blank lines.
Whitespace trimming markers
All tag types support optional trimming markers in the opening and/or closing delimiter.
-trims all whitespace (including newlines).~trims whitespace but keeps newlines.
{{-/{%-/{#-trims whitespace before the tag.-}}/-%}/-#}trims whitespace after the tag.{{~/{%~/{#~and~}}/~%}/~#}work the same, but do not remove line breaks.- Raw interpolations can be combined with trimming markers, e.g.
{{-= key -}}and{{~= key ~}}.
A \n {{- name -}} \nB -> AXB
A\n {{~ name ~}} \nB -> A\nX\nB
v={{-= html -}} -> v=<em>ok</em>
Robustness
Malformed or misplaced control tokens degrade to literal text instead of throwing at runtime.
In that case, the engine also keeps any following whitespace/newlines unchanged.
Custom filters
You can register custom filters at runtime. Unknown filters in templates are ignored; registered filters are applied left-to-right.
Registering a filter (Node/ESM)
import { registerTemplateFilter, renderTemplate } from 'crytemplate';
registerTemplateFilter('slug', (value) => {
const s = (value === undefined || value === null) ? '' : String(value);
return s.trim().toLowerCase().replace(/\s+/g, '-');
});
const out = renderTemplate('Hello {{ name | slug }}!', { name: 'John Doe' });
Registering a filter (browser)
<script src="https://cdn.jsdelivr.net/npm/crytemplate/dist/browser/crytemplate.min.js"></script>
<script>
cryTemplate.registerTemplateFilter('slug', (value) => {
const s = (value === undefined || value === null) ? '' : String(value);
return s.trim().toLowerCase().replace(/\s+/g, '-');
});
const out = cryTemplate.renderTemplate('Hello {{ name | slug }}!', { name: 'John Doe' });
</script>
Notes
- The handler signature is
(value: unknown, args?: (string | number | boolean | null)[]) => unknown. - Returning non-strings is allowed; the engine stringifies after the filter pipeline.
- Register filters once during application startup (the registry is global to the module).
- Re-registering the same name overrides the previous handler (including built-ins).