Overview
mylekha_template_engine is a Django/Jinja2-style HTML template rendering engine for Dart. Parse templates with {{ expression }} and {% tag %} syntax, evaluate them against runtime data, and stream back rendered HTML — with barcode generation, localization, and full template inheritance built in.
Features
- Django-style template syntax — variables
{{ }}, tags{% %}, filters| - 14 built-in tags —
assign,capture,cache,comment,cycle,extends,filter,for,if/unless,ifchanged,include,load,block,regroup - 40+ built-in filters — string, numeric, arithmetic, date/number formatting, collection operations, barcode/QR generation, Markdown-to-HTML, localization
- Template inheritance —
{% extends %}+{% block %}with unlimited depth - Barcode & QR codes — inline
data:image/png;base64,...output via| qrcode - Localization —
| trfilter with plural rules, nested keys, and link keys - Streaming render —
Stream<String>output, collected toFuture<String> - Fully extensible — custom filters, tags, modules, and template loaders
Installation
Add to your pubspec.yaml:
dependencies:
mylekha_html_renderer:
path: ../mylekha_html_renderer # or your pub source
Quick Start
Parse and render a template
import 'package:mylekha_html_renderer/mylekha_html_renderer.dart';
void main() async {
// Create a root context (auto-registers all built-in filters & tags)
final context = Context.create();
// Set template variables
context.variables['order'] = {
'number': 'ORD-001',
'total': 49.99,
'items': [
{'name': 'Widget A', 'qty': 2},
{'name': 'Widget B', 'qty': 1},
],
};
// Parse the template
final template = Template.parse(
context,
Source(null, '''
<h1>Order {{ order.number }}</h1>
<ul>
{% for item in order.items %}
<li>{{ item.name }} × {{ item.qty }}</li>
{% endfor %}
</ul>
<strong>Total: {{ order.total | moneyFormat: 2, "USD", 0 }}</strong>
''', null),
);
// Render to string
final html = await template.render(context);
print(html);
}
Load templates from disk
void main() async {
final context = Context.create();
context.variables['name'] = 'World';
// BuildPath resolves relative {% include %} and {% extends %} paths
final root = BuildPath(Uri.directory('/path/to/templates'));
final source = await root.resolve('hello.html');
final template = Template.parse(context, source);
print(await template.render(context));
}
Template Syntax at a Glance
Output a variable
{{ variable }}
{{ object.property }}
{{ list.0 }}
{{ "string literal" }}
Apply a filter
{{ price | moneyFormat: 2, "USD", 0 }}
{{ name | upper }}
{{ items | join: ", " }}
{{ code | qrcode: 300, 120, "Code128" }}
Tags
{% for item in items %}
{{ forloop.counter }}. {{ item.name }}
{% endfor %}
{% if user.active %}
Welcome, {{ user.name }}!
{% else %}
Please log in.
{% endif %}
{% assign total = price | multi: quantity %}
Whitespace control
Use - to trim surrounding whitespace:
{{- variable -}}
{%- for item in items -%}
Built-in Tags Summary
| Tag | Description |
|---|---|
assign | Scoped variable binding with filter expression RHS |
cache | Render block to string, store at root scope |
capture | Render block to string, store at current scope |
comment | Discard all inner content |
cycle | Rotate through values on each invocation |
extends | Inherit from a base template |
filter | Apply a filter to a block's output |
for | Iterate over a collection |
if / unless | Conditional rendering |
ifchanged | Render only when output differs from previous |
include | Embed another template |
load | Activate a registered module |
block | Named block for template inheritance |
regroup | Group a list by a field |
Built-in Filters Summary
| Category | Filters |
|---|---|
| String | upper, lower, capfirst, join, reversed, size, isEmpty, isNotEmpty, default, default_if_none |
| Numeric | parseInt, parseDouble, parseNum, abs, roundDouble, stringAsFixed |
| Arithmetic | add, minus, multi, divide, modulus |
| Mutating | self_add, self_minus, self_divide, self_multi |
| Formatting | date, number_format, moneyFormat |
| Collection | get, elementAt, generateList, mapOf, whereOf, foldOf |
| Content | markdownToHtml |
| Barcode/QR | qrcode |
| i18n | tr |
Troubleshooting
ParseException: ...missingRoot
{% include %} and {% extends %} require a Root to resolve relative paths. Pass a Root implementation in the Source constructor, or use BuildPath for file-system templates.
// ❌ fails for include/extends
Source(null, content, null)
// ✅ correct
final root = BuildPath(Uri.directory('/path/to/templates'));
final source = await root.resolve('template.html');
Filter silently produces empty output
The filter name may not be registered. Filter keys are case-sensitive. Check that the name exactly matches one in the filter reference, or that your custom module has been loaded with {% load moduleName %}.
Variable is null / empty in template
Variables are looked up by exact string key in context.variables. Nested access via . works on Map<String, dynamic>. If {{ order.number }} is empty, confirm:
context.variables['order']is set before callingrender()- The map key is
'number'(not'orderNumber'etc.)
include path not resolving
The path in {% include "partial.html" %} is resolved relative to the Root passed to the parent template's Source. Ensure both the parent and all included files share the same Root instance.
TagRenderException wrapping my filter error
Every tag/filter error is caught and re-thrown as TagRenderException. Access .error and .stacktrace on the exception to see the original cause.