Template::Jinja2

Complete Jinja2 template engine for Raku

Template::Jinja2

Complete Jinja2 template engine for Raku, targeting compatibility with the Python reference implementation (Jinja2 3.1). Designed for HuggingFace chat templates and general-purpose templating.

Synopsis

use Template::Jinja2;

my $env = Template::Jinja2.new;
my $tmpl = $env.from-string('Hello {{ name }}!');
say $tmpl.render(name => 'World');  # Hello World!

HuggingFace Chat Templates

use Template::Jinja2;
use JSON::Fast;

my $config = from-json('tokenizer_config.json'.IO.slurp);
my $chat-template = $config<chat_template>;

my $env = Template::Jinja2.new;
my $result = $env.from-string($chat-template).render(
    messages => [
        { role => 'system', content => 'You are helpful.' },
        { role => 'user', content => 'Hello!' },
    ],
    bos_token => '<s>',
    add_generation_prompt => True,
);

Produces byte-identical output to Python Jinja2 for ChatML, Llama 3, Mistral, Gemma 2, Zephyr, and Cohere Command A templates.

Features

Tags

All 15 Jinja2 tag types plus loop controls:

  • {% if %} / {% elif %} / {% else %} / {% endif %}

  • {% for %} / {% else %} / {% endfor %} with full loop context

  • {% set %} / {% endset %} with filter chains and namespace support

  • {% with %} / {% endwith %}

  • {% block %} / {% endblock %} with scoped blocks

  • {% extends %} / {% include %}

  • {% import %} / {% from ... import %}

  • {% macro %} / {% endmacro %} with defaults, varargs, kwargs

  • {% call %} / {% endcall %} with parameterized caller

  • {% filter %} / {% endfilter %}

  • {% do %} / {% raw %} / {# comment #}

  • {% autoescape %} / {% endautoescape %}

  • {% break %} / {% continue %}

Expressions

  • Arithmetic: +, -, *, /, //, %, **

  • Comparison: ==, !=, <, >, <=, >=>, chained

  • Logical: and, or, not

  • Membership: in, not in

  • Identity: is, is not

  • Ternary: expr if test else expr

  • String concat: ~

  • Filters: value | filter(args)

  • Subscript: items[0], items[-1], items[1:3], items[::-1]

  • Dot access: obj.attr, list.0

  • Literals: strings, integers, floats, hex/octal/binary, scientific notation, lists, dicts, tuples

Filters (52)

abs, attr, batch, capitalize, center, count, d/default, dictsort, e/escape, filesizeformat, first, float, forceescape, format, groupby, indent, int, items, join, last, length, list, lower, lstrip, map, max, min, pprint, random, reject, rejectattr, replace, reverse, round, rstrip, safe, select, selectattr, slice, sort, string, striptags, sum, title, tojson, trim, truncate, unique, upper, urlencode, urlize, wordcount, wordwrap, xmlattr

Tests (20+)

defined, undefined, none, boolean, integer, float, number, string, sequence, mapping, iterable, callable, sameas, eq/equalto, ne, lt/lessthan, le, gt/greaterthan, ge, even, odd, divisibleby, in, true, false, lower, upper

Template Inheritance

{# base.html #}
<html>{% block content %}default{% endblock %}</html>

{# child.html #}
{% extends "base.html" %}
{% block content %}{{ super() }} + override{% endblock %}

Supports multi-level inheritance with super() chaining.

Whitespace Control

{%- trim left -%}    {# trim both sides #}
{%+ no_lstrip +%}    {# disable lstrip/trim_blocks #}
my $env = Template::Jinja2.new(:trim-blocks, :lstrip-blocks);

Custom Delimiters

# ERB-style
my $env = Template::Jinja2.new(
    :block-start('<%'), :block-end('%>'),
    :variable-start('<%='), :variable-end('%>'),
    :comment-start('<%#'), :comment-end('%>'));

# PHP-style
my $env = Template::Jinja2.new(
    :block-start('<?'), :block-end('?>'),
    :variable-start('<?='), :variable-end('?>'),
    :comment-start('<!--'), :comment-end('-->'));

Loaders

use Template::Jinja2::Loader;

# File system
my $env = Template::Jinja2.new(
    loader => FileSystemLoader.new(searchpath => 'templates/'));

# In-memory
my $env = Template::Jinja2.new(
    loader => DictLoader.new(templates => {
        'base.html' => '...',
        'child.html' => '...',
    }));

Configuration

my $env = Template::Jinja2.new(
    :autoescape,                  # Auto-escape {{ }} output
    :trim-blocks,                 # Strip newline after block tags
    :lstrip-blocks,               # Strip leading whitespace before block tags
    :!keep-trailing-newline,      # Strip trailing newline from output
    :line-statement-prefix('#'),  # Line-based block syntax
    :line-comment-prefix('##'),   # Line-based comments
    globals => { site => 'My Site' },
    filters => { custom => sub ($v) { ... } },
    tests => { custom => sub ($v) { ... } },
);

Architecture

Template String -> [Lexer] -> Tokens -> [Parser] -> AST -> [Renderer] + Context -> Output
  • Lexer — Raku grammar tokenizer (default delimiters) or regex chunker (custom delimiters)

  • Parser — Pratt-style expression parser with block tag dispatch

  • AST — 30+ node types covering all Jinja2 constructs

  • Renderer — AST walker with expression evaluation, scope chain, filter/test dispatch

  • Context — Scope stack with Undefined sentinel and Namespace support

Testing

612 tests across 22 test files, including:

  • Ported tests from the Python Jinja2 reference test suite

  • Real-world HuggingFace chat template validation

  • Byte-identical output verification against Python Jinja2 3.1.6

# Run all tests
prove6 -I lib t/

# Run a single test
raku -I lib t/09-huggingface.rakutest

Author

Matt Doughty

License

Artistic-2.0

Template::Jinja2 v0.2.0

Complete Jinja2 template engine for Raku

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

JSON::Fast:ver<0.19>:auth<cpan:TIMOTIMO>

Provides

  • Template::Jinja2
  • Template::Jinja2::AST
  • Template::Jinja2::Context
  • Template::Jinja2::Exceptions
  • Template::Jinja2::Filters
  • Template::Jinja2::Lexer
  • Template::Jinja2::Loader
  • Template::Jinja2::Parser
  • Template::Jinja2::Renderer
  • Template::Jinja2::Tests

The Camelia image is copyright 2009 by Larry Wall. "Raku" is trademark of the Yet Another Society. All rights reserved.