binba
v0.0.7
Published
A modern, async-first template engine inspired by Jinja2, built for Bun with full support for async/await operations.
Readme
Binba - Jinja2-like Template Engine for Bun
A modern, async-first template engine inspired by Jinja2, built for Bun with full support for async/await operations.
Features
- 🚀 Async/Await Support: Native support for async filters and functions
- 📝 Jinja2-like Syntax: Familiar template syntax for Python developers
- ⚡ Built for Bun: Optimized for Bun's JavaScript runtime
- 🔧 Extensible: Easy to add custom filters and functions
- 🎯 TypeScript: Full TypeScript support with type definitions
Installation
bun installQuick Start
import { Template } from "./src/index";
// Simple variable rendering
const result = await Template.render("Hello, {{ name }}!", { name: "World" });
console.log(result); // "Hello, World!"
// With async filters
const result2 = await Template.render(
"{{ message | upper }}",
{ message: "hello" },
{
filters: {
upper: (value: string) => value.toUpperCase(),
},
}
);
console.log(result2); // "HELLO"Template Syntax
Variables
Variables are rendered using double curly braces:
{{ variable }}
{{ user.name }}
{{ items[0] }}Variable Assignment
Use {% set %} to assign variables within templates:
{% set name = "World" %}
Hello, {{ name }}!Variables can be assigned from async operations:
{% set doc = await zodula("123") %}
Document ID: {{ doc.id }}
Document Name: {{ doc.name }}Promises, thenables, and method chaining
Intermediate function and method calls do not unwrap Promises or thenables. Only an explicit await in the template (or output/set/if/for boundaries) adopts them. That way fluent APIs can return a thenable builder from one step and still call further methods on it before the final resolution:
{% set doc = await client.resource("Foo").load("id-1").withOptions(bypass) %}Variable output, {% set %}, {% if %}, and {% for %} each settle the expression once (via Promise.resolve), so {{ fn() }} still prints the resolved value when fn returns a Promise or thenable—you do not need to write {{ await fn() }} for that case.
Variables can also be assigned from function calls and filters:
{% set result = add(5, 3) %}
The sum is: {{ result }}
{% set data = "hello" | upper %}
Uppercase: {{ data }}Control Structures
If/Else
{% if user.logged_in %}
Welcome, {{ user.name }}!
{% else %}
Please log in.
{% endif %}For Loops
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}Loop variables are available:
{% for item in items %}
{{ loop.index }}. {{ item }}
{% if loop.first %} (First) {% endif %}
{% if loop.last %} (Last) {% endif %}
{% endfor %}Available loop variables:
loop.index- 1-based indexloop.index0- 0-based indexloop.first- true if first iterationloop.last- true if last iterationloop.length- total length
Filters
Apply filters using the pipe operator:
{{ message | upper }}
{{ price | formatCurrency }}
{{ text | truncate(50) }}Async Filters
Filters can be async functions:
const template = "{{ data | fetchData }}";
const result = await Template.render(template, { data: "user123" }, {
filters: {
fetchData: async (id: string) => {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 10));
return `User: ${id}`;
},
},
});Function Calls
Call functions directly in templates:
{{ getCurrentTime() }}
{{ formatDate(date) }}Functions can be async:
const result = await Template.render("{{ getTime() }}", {
getTime: async () => {
await new Promise(resolve => setTimeout(resolve, 10));
return new Date().toISOString();
},
});Expressions
Support for various expressions:
{{ a + b }}
{{ price * quantity }}
{{ user.age >= 18 }}
{{ isActive and isVerified }}
{{ not isHidden }}Logical Operators
Both Jinja2-style (and, or) and JavaScript-style (&&, ||) operators are supported:
{{ user.name || 'Anonymous' }}
{{ user.email || 'No email' }}
{{ user.active && 'Yes' || 'No' }}
{{ product.inStock || product.preOrder }}
{{ user.tags && user.tags.length > 0 }}Both styles work the same way - they return the first truthy value (for or/||) or the first falsy value (for and/&&), not boolean values.
Conditional Rendering
Conditionally render elements based on data:
{% if user.avatar %}
<img src="{{ user.avatar }}" alt="Avatar" />
{% endif %}
{% if user.email %}
<p>Email: <a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
{% endif %}
{% if user.bio %}
<p class="bio">{{ user.bio }}</p>
{% else %}
<p class="bio empty">No bio available</p>
{% endif %}
{% if user.tags && user.tags.length > 0 %}
<div class="tags">
{% for tag in user.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}Use || for fallback values:
<h2>{{ user.name || 'Anonymous User' }}</h2>
<p>{{ review.text || 'No review text' }}</p>
<span>Rating: {{ review.rating || 'N/A' }}</span>API
Template Class
import { Template } from "./src/index";
// Static method
const result = await Template.render(template, context, options);
// Instance method
const tpl = new Template(template, options);
const result = await tpl.render(context);Options
interface TemplateOptions {
filters?: FilterRegistry; // Custom filters
autoescape?: boolean; // Auto-escape HTML (default: true)
trimBlocks?: boolean; // Trim whitespace blocks
lstripBlocks?: boolean; // Strip leading whitespace
}Adding Filters
const tpl = new Template(template, {
filters: {
upper: (value: string) => value.toUpperCase(),
lower: (value: string) => value.toLowerCase(),
capitalize: (value: string) =>
value.charAt(0).toUpperCase() + value.slice(1),
},
});
// Or add filters dynamically
tpl.addFilter("reverse", (value: string) =>
value.split("").reverse().join("")
);Examples
See src/example.ts for comprehensive examples:
bun run src/example.tsDevelopment
# Run examples
bun run src/example.ts
# Run tests (if you add tests)
bun testLicense
MIT
