cryTemplate

Usage

ESM, CJS, or directly in the browser. All examples render plain strings.

ESM (Node.js / bundlers)
Import and render a template.
import { renderTemplate } from 'crytemplate';

const out = renderTemplate('Hello {{ name | trim | upper }}!', { name: '  Alex  ' });
// => "Hello ALEX!"
CJS (Node.js require)
Use CommonJS require().
const { renderTemplate } = require('crytemplate');

const out = renderTemplate('Hello {{ name }}!', { name: 'Alex' });
// => "Hello Alex!"
Browser (no bundler)
Use the IIFE bundle and the global cryTemplate object. You can load it from a CDN provider like jsDelivr or UNPKG.
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/crytemplate/dist/browser/crytemplate.min.js"></script>

<!-- UNPKG -->
<script src="https://unpkg.com/crytemplate/dist/browser/crytemplate.min.js"></script>

<script>
  const out = cryTemplate.renderTemplate('Hi {{ name }}!', { name: 'Alex' });
  console.log(out);
</script>
Parse once, render many (advanced)
Parse a template once and render it multiple times with different data.
import { renderTemplate, tplParse, tplRenderNodes } from 'crytemplate';

const nodes = tplParse('Hello {{ name | trim }}!');

const out1 = tplRenderNodes(nodes, [ { name: ' Alex ' } ]);
const out2 = tplRenderNodes(nodes, [ { name: ' Sam ' } ]);
// => "Hello Alex!" and "Hello Sam!"

// Scopes are a stack (array): last entry is the innermost scope.
const globals = { appName: 'cryTemplate' };
const data = { user: { name: 'Alex' } };
const out3 = tplRenderNodes(tplParse('App={{ appName }}, User={{ user.name }}'), [ globals, data ]);

// renderTemplate() also supports multiple scopes (rest args):
const out4 = renderTemplate('App={{ appName }}, User={{ user.name }}', globals, data);
Security note: Templates do not execute JavaScript. You cannot call functions from templates.

Syntax & Docs

Quick reference for the cryTemplate language. For the canonical, complete documentation see the repository README.

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)
Important: Interpolations do not evaluate JavaScript. Only identifier/dot-path lookups, fallbacks (||, ??) 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 ||.

Important: Tests are not JavaScript expressions. There is no arbitrary code execution.

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: \n and \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.

Caution: Custom filters run your code. If you generate raw HTML or introduce unsafe behavior, you can reintroduce injection risks.

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).

Try-it examples

Click “Run” to render. Output is shown as text.

Escaping vs raw
Shows why raw output is opt-in.
Escaped: {{ html }}
Raw: {{= html }}
{
  "html": "<strong>Trusted?</strong> & <script>alert(1)</script>"
}
Output

              
Filters
Trim + upper + dateformat.
Hello {{ name | trim | upper }}!
Today: {{ createdAt | dateformat('YYYY-MM-DD') }}
{
  "name": "  Alex  ",
  "createdAt": "2026-01-19T12:00:00Z"
}
Output

              
Fallbacks: || vs ??
See the semantic difference in one render.
nameOr: {{ name || 'fallback' }}
nameNullish: {{ name ?? 'fallback' }}
{
  "name": ""
}
Output

              
number() formatting
Decimals + separators.
US: {{ price | number(2, '.', ',') }}
DE: {{ price | number(2, ',', '.') }}
{
  "price": 1234.5
}
Output

              
json + urlencode
Serialize an object and encode it for a URL.
json: {{ obj | json }}
url: /search?q={{ obj | json | urlencode }}
{
  "obj": {
    "q": "ice cream",
    "page": 1
  }
}
Output

              
Whitespace trimming (- / ~)
Shows left/right trimming with interpolation and control tags.
[- removes newlines]
A
  {{- name -}}
B

[~ keeps newlines]
A
  {{~ name ~}}
B

[raw + trimming]
v= {{-= html -}}
end

[control tags]
X{% if show -%}
Y{% endif -%}
Z
X{%~ if show ~%}
Y{%~ endif ~%}
Z
{
  "name": "X",
  "html": "<em>ok</em>",
  "show": true
}
Output

              
Conditionals + loops
A practical snippet combining both.
{% if items %}
Items:
{% each items as it, i %}
  {{ i }}: {{ it | trim | replace(' ', '-') | lower }}
{% endeach %}
{% else %}
No items.
{% endif %}
{
  "items": [
    "  Coffee Beans  ",
    "Tea",
    "Brown Sugar"
  ]
}
Output

              
Want a full editor? Use the Playground.