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(...args?), 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(...args?)
Serializes the value to JSON using a custom safe stringifier. Unlike JSON.stringify, it does not execute toJSON() methods or getters, preventing code injection and side effects from malicious objects. Circular references are handled safely (emitted as null instead of crashing).
Arguments are detected by type and can be combined in any order: the string 'raw' disables the script-safe escaping, and a number sets the indentation for pretty-printed output.
By default the output is safe to embed inside a <script> block even with raw interpolation: <, >, & and U+2028/U+2029 are emitted as \uXXXX escapes (still valid, equivalent JSON), so a value like </script> cannot break out of the script context.
Use 'raw' only outside of an HTML/script context and when the exact byte representation matters (e.g. signing/hashing the raw string).
{{ obj | json }} // compact, script-safe, e.g. {"u":"\u003c/script\u003e"}
{{ obj | json(2) }} // indented with 2 spaces, script-safe
{{ obj | json('raw') }} // compact, plain JSON, e.g. {"u":""}
{{ obj | json('raw', 2) }} // indented, plain (order does not matter)
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.
An {% if %} or {% each %} block that is never closed is auto-closed at the end of the template, so its content is preserved instead of being silently dropped.
For untrusted templates, recursion depth is capped: deeply nested conditions, long negation chains and deeply nested if/each blocks degrade gracefully instead of throwing a stack-overflow error. These limits are far above any realistic template.
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).