Skip to main content

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.

Dart SDK Version

Features

  • Django-style template syntax — variables {{ }}, tags {% %}, filters |
  • 14 built-in tagsassign, 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| tr filter with plural rules, nested keys, and link keys
  • Streaming renderStream<String> output, collected to Future<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

TagDescription
assignScoped variable binding with filter expression RHS
cacheRender block to string, store at root scope
captureRender block to string, store at current scope
commentDiscard all inner content
cycleRotate through values on each invocation
extendsInherit from a base template
filterApply a filter to a block's output
forIterate over a collection
if / unlessConditional rendering
ifchangedRender only when output differs from previous
includeEmbed another template
loadActivate a registered module
blockNamed block for template inheritance
regroupGroup a list by a field

Built-in Filters Summary

CategoryFilters
Stringupper, lower, capfirst, join, reversed, size, isEmpty, isNotEmpty, default, default_if_none
NumericparseInt, parseDouble, parseNum, abs, roundDouble, stringAsFixed
Arithmeticadd, minus, multi, divide, modulus
Mutatingself_add, self_minus, self_divide, self_multi
Formattingdate, number_format, moneyFormat
Collectionget, elementAt, generateList, mapOf, whereOf, foldOf
ContentmarkdownToHtml
Barcode/QRqrcode
i18ntr

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:

  1. context.variables['order'] is set before calling render()
  2. 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.