π Browser-Optimized Fork: This is a customized version of Eta v3 specifically optimized for browser-only usage with unified includes and simplified template resolution.
Chuck is a lightweight and blazing fast embedded JS templating engine optimized for browser use. This fork is specifically tailored for browser-only usage with simplified APIs and smaller bundle sizes. It's written in TypeScript and emphasizes great performance, configurability, and ease of use.
- π¦ 0 dependencies
- π‘ Only ~3.5 KB minzipped (browser-optimized)
- β‘οΈ Written in TypeScript
- π Browser-first design with simplified APIs
- π Super Fast
- π§ Configurable
- Plugins, custom delimiters
- Always-on caching (no configuration needed)
- π¨ Powerful
- Precompilation, unified includes
- Layout support!
- No @ prefix required for template names
- Twig-like syntax for familiar for loops and conditionals
- Twig-inspired filters with pipe syntax
- Simplified error handling for browser environments
- π₯ Reliable
- Better quotes/comments support
- ex.
{{ someval + "string }}" }}compiles correctly, while it fails with doT or EJS
- ex.
- Great error reporting
- Better quotes/comments support
- β‘οΈ Exports ES Modules as well as UMD
- π Easy template syntax
Chuck uses Twig-inspired delimiters:
{{ }}- Output/interpolation (escapes HTML by default){% %}- Tags/expressions/code blocks{%~ %}- Raw output (no HTML escaping)
<h1>{{ it.title }}</h1>
{% for (var i = 0; i < it.items.length; i++) { %}
<p>{{ it.items[i].name }}: {%~ it.items[i].description %}</p>
{% } %}
{% if (it.user.isAdmin) { %}
<p>Welcome, admin!</p>
{% } %}This fork includes Twig-inspired syntax for more familiar templating patterns alongside the existing JavaScript syntax:
Simple Iteration:
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}Key-Value Iteration:
{% for key, value in data %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}Complex Expressions:
{% for pagenum in it.middle %}
<span>Page {{ pagenum }}</span>
{% endfor %}
{% for item in it.getPages() %}
<div>{{ item }}</div>
{% endfor %}
{% for page in it.pages %}
<li>{{ page.title }}</li>
{% endfor %}Basic If Statement:
{% if it.user.isAdmin %}
<p>Admin Panel</p>
{% endif %}If-Else:
{% if it.user.isActive %}
<span>Active User</span>
{% else %}
<span>Inactive User</span>
{% endif %}If-Elsif-Else:
{% if it.user.role === 'admin' %}
<p>Administrator</p>
{% elsif it.user.role === 'moderator' %}
<p>Moderator</p>
{% else %}
<p>Regular User</p>
{% endif %}Compound Expressions:
{% if (it.user.isActive && it.user.role === 'admin') || it.user.isSuperUser %}
<p>Admin Access Granted</p>
{% elsif it.user.isActive && (it.user.role === 'moderator' || it.user.role === 'editor') %}
<p>Staff Access Granted</p>
{% else %}
<p>Regular Access</p>
{% endif %}Function Calls:
{% if it.user.hasMethod() || it.user.isAdmin() %}
<p>Permission Granted</p>
{% elsif it.items.length > 0 && it.hasPermission('view') %}
<p>Content Available</p>
{% else %}
<p>No Access</p>
{% endif %}Twig-style Comments (Single & Multi-line):
{# This is a single-line comment #}
{#
This is a multi-line comment
that can span multiple lines
and won't appear in output
#}
Hello {# inline comment #} World
<!-- Outputs: Hello World -->
{# Comments can contain {{ variables }} and {% code %} that won't be processed #}Both syntaxes work seamlessly together:
<!-- Twig-style loops -->
{% for item in items %}
<!-- JavaScript-style conditions -->
{% if (item.featured) { %}
<strong>{{ item.name }}</strong>
{% } else { %}
{{ item.name }}
{% } %}
{% endfor %}Here's a comprehensive example combining Twig syntax with filters:
const chuck = new Chuck();
const template = `
<div class="user-profile">
<h1>{{ it.user.name | upper }}</h1>
{% if it.user.isActive %}
<span class="badge active">{{ it.user.status | capitalize }}</span>
{% else %}
<span class="badge inactive">Inactive</span>
{% endif %}
<div class="stats">
<p>Posts: {{ it.user.posts | length }}</p>
<p>Joined: {{ it.user.joinDate | default('Unknown') }}</p>
</div>
{% if it.user.posts | length > 0 %}
<h2>Recent Posts</h2>
<ul>
{% for post in posts %}
<li>
<strong>{{ post.title | trim }}</strong>
<small>({{ post.views | default(0) }} views)</small>
{% if post.featured %}
<span class="featured">β Featured</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>No posts yet.</p>
{% endif %}
</div>
`;
const result = chuck.renderString(template, {
user: {
name: 'john doe',
isActive: true,
status: 'verified',
joinDate: '2024-01-15',
posts: [
{ title: ' My First Post ', views: 42, featured: true },
{ title: 'Another Article', views: null, featured: false }
]
},
posts: [
{ title: ' My First Post ', views: 42, featured: true },
{ title: 'Another Article', views: null, featured: false }
]
});This fork includes powerful Twig-inspired filter support with pipe syntax for data transformation:
{{ it.name | upper }}
{{ it.price | round(2) }}
{{ it.text | trim | capitalize }}String Filters:
upper- Convert to uppercaselower- Convert to lowercasecapitalize- Capitalize first lettertrim- Remove whitespace
Number Filters:
round(decimals)- Round to specified decimal placesabs- Absolute value
Array/Object Filters:
length- Get length of arrays or object keysjoin(separator)- Join array elements with separator
Utility Filters:
default(value)- Use fallback value if empty/nulljson- Convert to JSON string
<!-- String transformations -->
<h1>{{ it.title | upper }}</h1>
<p>{{ it.description | trim | capitalize }}</p>
<!-- Number formatting -->
<span>Price: ${{ it.price | round(2) }}</span>
<!-- Array operations -->
<p>Items: {{ it.tags | join(', ') }}</p>
<span>Total: {{ it.items | length }} items</span>
<!-- Fallback values -->
<p>{{ it.username | default('Guest') }}</p>
<!-- Raw JSON output -->
<script>const data = {%~ it.config | json %};</script>const chuck = new Chuck();
// Simple filter
chuck.addFilter('reverse', (text) => {
return String(text).split('').reverse().join('');
});
// Filter with arguments
chuck.addFilter('repeat', (text, times) => {
return String(text).repeat(times);
});
// Advanced filters
chuck.addFilter('slugify', (text) => {
return String(text)
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
});
chuck.addFilter('currency', (value, symbol = '$', decimals = 2) => {
return symbol + parseFloat(value).toFixed(decimals);
});
// Usage in templates
chuck.renderString('{{ it.title | slugify }}', { title: 'Hello World!' });
// Output: "hello-world"
chuck.renderString('{{ it.price | currency("β¬") }}', { price: 19.99 });
// Output: "β¬19.99"Combine multiple filters for complex transformations:
{{ it.userInput | trim | lower | capitalize }}
{{ it.items | join(' ') | upper | trim }}
{{ it.tags | length | default(0) }}// Add custom filter
chuck.addFilter(name, function);
// Check if filter exists
chuck.hasFilter(name);
// Get filter function
chuck.getFilter(name);
// Remove filter
chuck.removeFilter(name);const chuck = new Chuck();
// Register multiple custom filters
chuck.addFilter('excerpt', (text, length = 100) => {
return text.length > length ? text.substring(0, length) + '...' : text;
});
chuck.addFilter('fileSize', (bytes) => {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
});
// Use in complex template
const template = `
<article>
<h1>{{ it.title | upper }}</h1>
<p>{{ it.content | excerpt(150) }}</p>
<small>Size: {{ it.fileSize | fileSize }}</small>
</article>
`;
const result = chuck.renderString(template, {
title: "my blog post",
content: "Lorem ipsum dolor sit amet...",
fileSize: 1048576
});Chuck is optimized for browser use with only ~3.5KB minified+gzipped:
This fork includes special optimizations for browser-only usage:
- π Unified Include Directive: No more
includeAsync- just useinclude() - π No @ Prefix Required: Templates can be referenced by simple names like
"header"instead of"@header" - π Backward Compatible: Existing code with
@prefixes still works - πΎ Always-On Caching: Templates are automatically cached - no configuration needed
- β‘ Simplified Errors: Browser-optimized error handling without filepath dependencies
- π¦ Smaller Bundle: Removed server-side filesystem resolution and unnecessary features
ES Modules (Recommended):
<script type="module">
import { Chuck } from './dist/browser.module.mjs';
const chuck = new Chuck();
// Direct string rendering
const result = chuck.renderString('Hi {{ it.name }}!', { name: 'World' });
document.body.innerHTML = result;
// Pre-load templates for reuse (no @ prefix needed!)
chuck.loadTemplate('greeting', 'Hello {{ it.name }}, welcome to {{ it.site }}!');
chuck.loadTemplate('header', 'Header: {{ it.title }}');
chuck.loadTemplate('page', 'Page: {%~ include("header", {title: "My Site"}) %} - Content: {{ it.content }}');
const greeting = chuck.render('greeting', { name: 'Alice', site: 'our site' });
const page = chuck.render('page', { content: 'Welcome!' });
// Templates are automatically cached - no configuration needed!
</script>UMD (Universal):
<script src="./dist/browser.umd.js"></script>
<script>
const chuck = new window.chuck.Chuck();
// Simple rendering
const result = chuck.renderString('Hi {{ it.name }}!', { name: 'World' });
// Template with includes (unified directive)
chuck.loadTemplate('nav', 'Nav: {{ it.links }}');
const withInclude = chuck.renderString('Page: {%~ include("nav", {links: "Home | About"}) %}', {});
document.body.innerHTML = result + '<br>' + withInclude;
</script>