A fast, spec-compliant Mustache template engine for JavaScript / TypeScript that compiles templates into native JavaScript functions for high-throughput rendering.
- Compiled templates — each template is compiled once to a plain JS function; subsequent renders are pure function calls with no parsing overhead
- Full Mustache syntax — variables, sections, inverted sections, comments, partials, dynamic partials, and set-delimiter tags
- Standalone tag whitespace stripping — section, comment, partial, set-delimiter, and inheritance tags on their own line consume that whole line (leading whitespace + trailing newline), per spec
- Template inheritance —
{{< parent}}/{{$ block}}for layout composition (extends the base Mustache spec) - Permissive tag whitespace — spaces are allowed before and after any tag
sigil:
{{ # name }},{{> partial }},{{ = [ ] = }}, etc. - Async rendering —
renderAsync/renderCompiledAsyncresolve async and Promise-returning view values automatically, with lazy async partial loading - TypeScript-first — full type declarations included, zero runtime dependencies
- No extra dependencies — only the Node.js built-in
node:testrunner is used for tests
npm install abdk-mustache-jsimport { render } from "abdk-mustache-js";
const html = render("Hello, {{name}}!", { name: "World" });
// → "Hello, World!"Compile once, render many times:
import { compile, renderCompiled } from "abdk-mustache-js";
const tmpl = compile("Hello, {{name}}!");
console.log(renderCompiled(tmpl, { name: "Alice" })); // Hello, Alice!
console.log(renderCompiled(tmpl, { name: "Bob" })); // Hello, Bob!render("{{#show}}visible{{/show}}", { show: true }); // "visible"
render("{{#show}}visible{{/show}}", { show: false }); // ""
render("{{^empty}}fallback{{/empty}}", { empty: [] }); // "fallback"Arrays are iterated automatically:
render(
"{{#items}}- {{name}}\n{{/items}}",
{ items: [{ name: "One" }, { name: "Two" }] }
);
// "- One\n- Two\n"import { render } from "abdk-mustache-js";
const output = render(
"{{> header}}Content{{> footer}}",
{ title: "Home", year: 2026 },
{
header: "<h1>{{title}}</h1>\n",
footer: "<footer>{{year}}</footer>",
}
);import { compile, renderCompiled } from "abdk-mustache-js";
const layout = compile(`
<html>
<head><title>{{$title}}Default Title{{/title}}</title></head>
<body>{{$body}}{{/body}}</body>
</html>
`);
const page = compile(`
{{< layout}}
{{$title}}My Page{{/title}}
{{$body}}<p>Hello, {{name}}!</p>{{/body}}
{{/layout}}
`);
renderCompiled(page, { name: "Alice" }, { layout });The spec says everything except block overrides should be silently ignored inside a parent. This implementation is deliberately stricter on errors but also more useful:
| Content | Behaviour |
|---|---|
| Plain text | Silently ignored |
Comments {{! … }} |
Silently ignored |
Block overrides {{$ block }} … {{/block}} |
Processed normally — overrides the named block in the parent template |
Set-delimiter tags {{= … =}} |
Processed — delimiter change takes effect for the rest of the parent body, allowing different delimiters for different block overrides |
Sections {{#}} / {{^}} |
Compile-time error — hiding structural tags would conceal bugs |
Variables {{name}} / {{{name}}} / {{& name}} |
Compile-time error |
Partials {{> …}} / {{> *…}} |
Compile-time error |
Nested parents {{< …}} |
Compile-time error |
Example — changing delimiters mid-parent to override two blocks with different delimiter styles:
const parent = compile("{{$a}}A{{/a}} {{$b}}B{{/b}}");
const child = compile("{{< parent}}{{=[ ]=}}[$a]X[/a][={{ }}=]{{$b}}Y{{/b}}{{/parent}}");
renderCompiled(child, {}, { parent }); // "X Y"Tags that appear alone on a line (with only optional leading whitespace) are
treated as standalone: the entire line, including the leading whitespace
and the trailing newline (\n or \r\n), is removed from the output. This
applies to section, inverted-section, comment, set-delimiter, partial, block,
and parent tags. Variable tags are never standalone.
Template:
| before
{{#show}}
| inside
{{/show}}
| after
Output (show = true):
| before
| inside
| after
When a standalone partial tag has leading whitespace, that whitespace is
prepended to every line of the included partial's output using
String.replace(/^/gm, indentation). The standalone tag line's own trailing
newline is then taken from the template itself — no newline characters are
introduced that were not already present in the template or the partial.
Template:
| before
{{> item}}
| after
Partial ("item"): "line1\nline2"
Output:
| before
line1
line2
| after
Because /^/gm matches the position after every embedded newline (including
the one that terminates a trailing newline in the partial), a partial whose
content ends with \n produces an indented empty line before the
template's own newline:
| Partial content | Output of " {{> p}}\n" |
|---|---|
"foo" (no trailing \n) |
" foo\n" |
"foo\n" (trailing \n) |
" foo\n \n" |
"foo\nbar" |
" foo\n bar\n" |
"foo\nbar\n" |
" foo\n bar\n \n" |
"" (empty / missing) |
" \n" |
Non-standalone (inline) partials — where the tag shares a line with other content — are inserted as-is with no indentation.
Spaces are permitted before and after any tag sigil, and around any tag name. All three positions are equivalent:
{{ name }} — spaces around name
{{# section }} — space after sigil
{{ # section }} — space before and after sigil
{{> * dynamic }} — spaces around * in dynamic partial
{{ = [ ] = }} — spaces around = in set-delimiter
This applies uniformly to all tag types: variables, sections, inverted sections, comments, partials, dynamic partials, blocks, parents, and set-delimiter tags.
Use triple mustaches {{{…}}} or {{& …}} to skip HTML escaping:
render("{{{html}}}", { html: "<b>bold</b>" }); // "<b>bold</b>"render("{{val}}", { val: "x" }, {}, s => s); // disable escaping entirelyrenderAsync and renderCompiledAsync resolve any view property that is an
async / Promise-returning function. Partials can also be loaded asynchronously.
import { renderAsync } from "abdk-mustache-js";
const html = await renderAsync(
"Hello, {{name}}! You have {{count}} messages.",
{
name: async () => fetchUserName(),
count: async () => fetchMessageCount(),
}
);Async partials (loaded on demand via a loader function):
import { renderAsync } from "abdk-mustache-js";
const html = await renderAsync(
"{{> header}}{{body}}",
{ body: "Content" },
// loader function — called once per partial name, result cached
async (name) => fetchTemplate(name)
);Or pass a plain object of pre-compiled string partials:
const html = await renderAsync(
"{{> header}}{{body}}",
{ body: "Content" },
{ header: "<h1>ABDK</h1>" }
);Parses the template string and returns a compiled function. Throws a
descriptive Error if the template is syntactically invalid (unmatched tags,
unclosed sections, etc.).
Renders a pre-compiled template synchronously.
| Parameter | Type | Default |
|---|---|---|
template |
CompiledTemplate |
— |
view |
any |
— |
partials |
{ [name: string]: CompiledTemplate } | (name: string) => CompiledTemplate | null |
{} |
escape |
(s: string) => string |
built-in HTML escape |
Compiles and renders in one call. Accepts string partials (compiled lazily on
first use per render call).
| Parameter | Type | Default |
|---|---|---|
template |
string |
— |
view |
any |
— |
partials |
{ [name: string]: string } | (name: string) => string | null |
{} |
escape |
(s: string) => string |
built-in HTML escape |
Renders a pre-compiled template; asynchronous view property functions are
resolved automatically (re-rendering until all promises settle). When a loader
function is supplied, it is called at most once per partial name per
renderCompiledAsync call.
| Parameter | Type | Default |
|-----------|------|---------||
| template | CompiledTemplate | — |
| view | any | — |
| partials | { [name: string]: CompiledTemplate } | (name: string) => Promise<CompiledTemplate \| null> | {} |
| escape | (s: string) => string | built-in HTML escape |
Convenience wrapper around renderCompiledAsync that accepts a template
string and string partials or an async loader function.
| Parameter | Type | Default |
|-----------|------|---------||
| template | string | — |
| view | any | — |
| partials | { [name: string]: string } | (name: string) => Promise<string \| null> | {} |
| escape | (s: string) => string | built-in HTML escape |
type CompiledTemplate = (
view: any[],
partials: { [name: string]: CompiledTemplate }
| ((name: string) => CompiledTemplate | null),
blocks: { [name: string]: () => string },
escape: (string: string) => string
) => string;| Feature | Supported |
|---|---|
Variables {{name}} |
✓ |
Unescaped {{{name}}} / {{& name}} |
✓ |
Sections {{#name}}…{{/name}} |
✓ |
Inverted sections {{^name}}…{{/name}} |
✓ |
Comments {{! … }} |
✓ |
Partials {{> name}} |
✓ |
Dynamic partials {{> *name}} |
✓ |
Set delimiters {{= <% %> =}} |
✓ |
Template inheritance {{< parent}} / {{$ block}} |
✓ (extension) |
| Standalone tag whitespace stripping | ✓ |
| Whitespace before/after any tag sigil | ✓ (extension) |
| Lambda sections (raw text + render callback) | ✗ (optional spec feature) |
| Partial indentation (standalone partial prepends indent) | ✓ |
MIT © ABDK Consulting