Tiny EJS-like templating library with whitespace normalization.
I wasn't satisfied with EJS's implementation or with any other HTML templating libraries, so I wrote (and re-wrote) templaterrr to satisfy my requirements. It's tiny, clean, simple, and the templated HTML has appropriate whitespace for my taste. For most server-side applications, you can just import the render function and call it as part of your application's routes.
This library ships as a CommonJS bundle with full TypeScript definitions, but it can be used with import on modern Node.js or Bun. The test suite uses Bun's built-in testing library.
npm install templaterrrThe simplest way to use templaterrr is with the render function, which loads and renders a template file in one step:
import { render } from 'templaterrr';
var html = render('views/welcome.ejs', { name: 'Alice' });
console.log(html);
// Output: <h1>Hello, Alice!</h1>Template syntax uses <%= %> for escaped output and <% %> for code:
<!-- views/welcome.ejs -->
<h1>Hello, <%= name %>!</h1>
<ul>
<% items.forEach(function(item) { %>
<li><%= item %></li>
<% }); %>
</ul>For templates defined as strings in your code, use compileString:
import { compileString } from 'templaterrr';
var template = compileString('<h1><%= title %></h1><p><%= content %></p>');
// Reuse the compiled template multiple times
var html1 = template({ title: 'Hello', content: 'World!' });
var html2 = template({ title: 'Goodbye', content: 'For now!' });
console.log(html1);
// Output: <h1>Hello</h1><p>World!</p>
console.log(html2);
// Output: <h1>Goodbye</h1><p>For now!</p>Use <%- include(path, context) %> to include other templates. Note the - to prevent HTML escaping:
<!-- views/page.ejs -->
<!DOCTYPE html>
<html>
<%- include('views/header.ejs', { title: pageTitle }) %>
<body>
<p>Welcome, <%= userName %>!</p>
</body>
</html><!-- views/header.ejs -->
<header>
<h1><%= title %></h1>
</header>import { render } from 'templaterrr';
var html = render('views/page.ejs', {
pageTitle: 'My Site',
userName: 'Alice'
});Customize template behavior with options:
import { compileFile, render } from 'templaterrr';
// Set base directory and extension for template resolution
var template = compileFile('header', {
baseDirectory: './views',
extension: '.html'
});
// Looks for './views/header.html'
// Disable caching during development
var html = render('template.ejs', { data: 'value' }, {
useCache: false
});
// Preserve all whitespace (disable normalization)
var template = compileString('<p> spaces </p>', {
preserveWhitespace: true
});
// Use a custom escape function
var template = compileString('<%= text %>', {
escape: function(value) {
return String(value).toUpperCase();
}
});Renders a template file directly to a string in a single operation.
Parameters:
path: string- Path to the template filecontext: any- Context object to use when renderingoptions: Options- Optional compilation options
Returns: string - The rendered HTML
Example:
var html = render('welcome.ejs', { name: 'Alice' });Compiles a template source string into a reusable render function.
Parameters:
source: string- Template source codeoptions: Options- Optional compilation options
Returns: (context: any) => string - The compiled template function.
Example:
var template = compileString('<h1><%= title %></h1>');
var html = template({ title: 'Hello' });Compiles a template from a file into a reusable render function. Compiled templates are cached by default.
Parameters:
path: string- Path to the template fileoptions: Options- Optional compilation options
Returns: (context: any) => string - The compiled template function.
Example:
var template = compileFile('views/header.ejs');
var html = template({ title: 'Welcome' });Compiles a template source string into generated JavaScript code without executing it. Useful for build pipelines or code inspection.
Parameters:
name: string- Function name for the generated code, ornullfor just the bodysource: string- Template source codeoptions: Options- Optional compilation options
Returns: string - The generated JavaScript code
Example:
var code = compileToString('myTemplate', '<h1><%= title %></h1>');
console.log(code);
// function myTemplate(__context, __escape, __include) {
// var __buffer = "";
// ...
// return __buffer;
// }| Option | Type | Default | Description |
|---|---|---|---|
| baseDirectory | string |
"" |
Base directory for resolving template paths when including other templates. Ignored if custom readFile is provided. |
| extension | string |
".ejs" |
File extension to append when resolving template names. Ignored if custom readFile is provided. |
| readFile | (path) => string |
null |
Custom function to read file contents. |
| escape | (string) => string |
null |
Custom function to escape values in <%= %> tags. Default escapes HTML entities. |
| include | (path, context) => string |
null |
Custom function to include other templates. |
| useCache | boolean |
true (production) / false (development) |
Whether to cache compiled templates. |
| preserveWhitespace | boolean |
false |
Whether to preserve all whitespace. When false, whitespace is normalized. |
| templatePath | string |
"<anonymous template>" |
Path to use in error messages. Ignored/overridden when compiling from a file. |
Escaped output:
<%= value %>Outputs the value with HTML escaping (&, <, >, ", ' are escaped).
Unescaped output:
<%- value %>Outputs the value without escaping.
Code execution:
<% var x = 10; %>
<% if (condition) { %>
<p>True</p>
<% } %>Executes JavaScript code without output.
Including templates:
<%- include('path/to/template.ejs', { key: 'value' }) %>Includes another template with its own context data.
templaterrr uses EJS-like syntax but is not a drop-in replacement for EJS. It implements a subset of EJS features:
Supported:
- Basic template tags:
<% %>,<%= %>,<%- %> - Including templates via
includefunction - JavaScript expressions and control flow
- Context object access
Missing:
- Async/await in templates
- Custom delimiters (only
<% %>syntax) - Filters
- Advanced EJS features (layouts, blocks, etc.)
If you need these features, use the full EJS library instead.
By default, compiled templates are cached in memory for performance:
NODE_ENV !== "development": Caching enabled by defaultNODE_ENV === "development": Caching is disabled in development
You can override this with the useCache option:
// Always use cache
var template = compileFile('template.ejs', { useCache: true });
// Never use cache
var template = compileFile('template.ejs', { useCache: false });The cache is never cleared. If you're loading many unique templates, turn it off.
By default with preserveWhitespace: false, templaterrr normalizes whitespace to produce clean HTML:
- Multiple spaces/tabs are collapsed to a single space
- Multiple newlines are collapsed to a single newline
- Whitespace is dedented based on common indentation
- Whitespace inside
<pre>,<textarea>,<script>,<style>, and<!--comments-->is preserved, but still dedented
Example:
var template = compileString(`
<div>
<p>Hello World</p>
<p>Multiple
newlines</p>
</div>
`);
console.log(template({}));
// Output:
//
// <div>
// <p>Hello World</p>
// <p>Multiple
// newlines</p>
// </div>
//To preserve all whitespace exactly as written, use preserveWhitespace: true:
var template = compileString('hello world\n\n\nthere', {
preserveWhitespace: true
});
console.log(template({}));
// Output: hello world\n\n\nthere</script> inside <script> tag strings:
The parser does not understand JavaScript string literals inside <script> tags. If you have "</script>" in your JavaScript code, it will incorrectly close the script tag:
<script>
var code = "</script>"; // this breaks!
var more = " so many spaces ";
// will print " so many spaces " oh no!
console.log(more);
</script>The simple workaround is to split the closing tag or escape it:
<script>
var code = "<" + "/script>";
var more = " so many spaces ";
// will print " so many spaces " as expected
console.log(more);
</script>Note that this also affects <pre>, <style>, and <textarea> tags.
Licensed under the MIT License.
Made with ❤ by Lua (foxgirl.dev).