Catalog · How the catalog works
ginger
Modern Jinja2 template engine for FPC. Seamless integration with mORMot 2 & Brook frameworks for high-speed HTML rendering.
Anyone may press Interesting, signed in or not. It helps others find this project.
README
Ginger
Ginger is a template engine for Free Pascal / Lazarus that implements
Jinja2's template syntax — the same {{ }}, {% if %}, {% for %},
filters, macros, and template inheritance that Python/Flask/Django
developers already know — as a native FPC library, with no Python
runtime involved.
It exists so an FPC web application can render HTML (or any other
text format) the same way a modern Python or Node app does: designers
and frontend developers write .html templates in a syntax that is
widely documented and tool-supported, and the backend renders them
through a fast, native, memory-checked library.
Status
Pre-1.0, unpublished, no external users yet. The engine itself is
solid: 648 automated tests, 0 unfreed memory blocks on every run, and
every feature is implemented against a ground-truthed reading of real
Jinja2's own source (not guessed from documentation) — see
docs/adr/ for the full, numbered design history, including every
accepted gap versus upstream Jinja2. Licensed BSD-3-Clause (see
LICENSE).
Feature parity target: Jinja2 3.1.6 specifically, not "whatever
Jinja2 does" in the abstract. Every ADR's ground-truthing was done
against a real, installed jinja2==3.1.6, and every behavior this
README or an ADR calls "matching upstream"/"ground-truthed" means
matching that exact release — not a later or earlier Jinja2 version,
and not the Jinja2 docs, which lag and occasionally disagree with the
installed package's actual behavior. A future Jinja2 release changing
some edge case does not retroactively make Ginger's 3.1.6-ground-
truthed behavior wrong; revisiting the target version is a deliberate
decision for a future ADR, not something that happens silently.
Why Jinja2 syntax, specifically
Jinja2 is the most widely used template language in the Python web world (Flask's default, and Django's own syntax is a close cousin). Implementing that syntax in Pascal — rather than inventing a new template language — means:
- Existing
.html/.txt/.xmltemplates, editor syntax highlighting, and documentation written for Jinja2 mostly apply as-is. - A team with any Python/Flask/Django background can write Ginger templates on day one.
- Design decisions (operator precedence, autoescaping rules,
whitespace control, how
{% for %}handles a missing variable, and so on) have a known-correct reference implementation to check against, instead of being invented from scratch.
Ginger is not a Python binding and does not embed Python. It is a from-scratch Pascal implementation of the lexer, parser, and evaluator for that syntax.
101: getting started with no Python or Jinja background required
A template is a text file with ordinary content plus a small number of special markers:
{{ expression }}— prints a value.{% statement %}— control flow: conditionals, loops, variable assignment, reusable blocks.{# comment #}— removed from the output entirely.
That's the whole syntax surface for everyday use. A minimal template:
Hello, {{ name }}!
{% if items %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
<p>No items.</p>
{% endif %}Rendering it from Pascal
Ginger's own package unit is GingerLib (src/GingerLib.lpk); add it
as a dependency of your project (or add its individual units,
listed below, to your project/package manually). The units you'll
touch directly for basic rendering are GingerLoader, GingerContext,
and GingerValue.
program HelloGinger;
{$mode objfpc}{$H+}
uses
SysUtils, GingerLoader, GingerContext, GingerValue;
var
Loader: TGingerDictLoader;
Env: TGingerEnvironment;
Ctx: TGingerContext;
Items: TGingerValueArray;
begin
// A TGingerDictLoader keeps templates in memory by name -- handy
// for embedding templates in your binary or for quick experiments.
// For templates that live as files on disk, use
// TGingerFileSystemLoader(BaseDir) instead; both expose the same
// Load method shape, so either can be assigned to Env.Loader.
Loader := TGingerDictLoader.Create;
Loader.SetTemplate('hello.html',
'Hello, {{ name }}!' + LineEnding +
'{% for item in items %}- {{ item }}' + LineEnding + '{% endfor %}');
Env := TGingerEnvironment.Create;
Env.Loader := @Loader.Load;
Env.DefaultAutoescape := True; // escape HTML-unsafe characters by default
Ctx := TGingerContext.Create;
try
Ctx.SetVar('name', GingerStr('World'));
Items := nil;
SetLength(Items, 2);
Items[0] := GingerStr('first');
Items[1] := GingerStr('second');
Ctx.SetVar('items', GingerList(Items));
WriteLn(Env.RenderTemplate('hello.html', Ctx));
finally
Ctx.Free;
Env.Free;
Loader.Free;
end;
end.That's the entire integration surface for a basic use case: a loader
to find template source by name, an environment to parse/cache
templates and drive the render, a context to hold the variables a
template sees, and GingerValue's Ginger* constructors (GingerStr,
GingerInt, GingerFloat, GingerBool, GingerList,
GingerEmptyDict/GingerDictSet, GingerNone) to build those
variables' values.
What's supported
Expressions (arithmetic, comparisons, and/or/not, string
concatenation with ~, attribute/index access, ternary a if b else c), the standard control-flow tags (if/elif/else, for/else,
set, both inline and block forms, with, autoescape), template
composition (extends/block/super(), include, import/from import), macros (including the catch-all varargs/kwargs
parameters and *args/**kwargs unpacking at any call site — a plain
call, a |filter(...), or an is test(...)), tuple-target
{% set a, b = ... %}/{% for a, b in ... %} (a string or dict RHS
unpacks per-character/per-key, same as a list), plain {% print %}, a library of built-in
filters and tests, globals including range/dict/cycler/joiner/
namespace/lipsum, {% raw %}, and whitespace control (-/+
markers, trim_blocks/lstrip_blocks). String literals support
\n/\t/\r/\a/\b/\f/\v, \'/\"/\\, 1-3-digit \ooo
octal escapes, and \xHH/\uXXXX/\UXXXXXXXX Unicode escapes; any
other \ is kept literally (\q stays \q, matching upstream).
Every accepted gap versus real Jinja2 — what's deliberately not
implemented yet, and why — is listed in the "Consequences" section of
the relevant ADR under docs/adr/; nothing is silently missing
without a documented reason.
Deliberate divergences (opt-in)
A handful of behaviors depart from upstream Jinja2 on purpose, gated behind an explicit opt-in so a template that doesn't ask for them behaves exactly like real Jinja:
Ctx.TitlecaseWords— thetitlefilter uses true Unicode titlecasing instead of a plain uppercase-first rule, only when set. This also covers the Latin digraphs used in Serbian/Croatian orthography (dž/lj/nj) and Czech/Slovak dz, each mapped to its own distinct titlecase codepoint rather than plain uppercase — no separate locale needed, since these are specific precomposed codepoints, not a language-wide rule.Ctx.Locale = 'tr'/'az'— Turkish/Azeri "dotted/dotless I" casing exceptions (plain ASCIII/imap differently than Unicode's locale-independent default).Ctx.Locale = 'lt'— Lithuanian casing exceptions (dot-above preservation/removal around accented letters).Ctx.Locale = 'nl'— Dutch "ij" digraph capitalization rules fortitle/capitalize.
See the relevant ADRs under docs/adr/ (ADR-0027 through ADR-0031)
for the ground-truthed details of each.
Custom filters and tests
GingerFilters exports the same registration calls the built-ins
themselves are wired up through, so a user-defined filter or is
test is a first-class citizen, not a workaround:
uses GingerValue, GingerFilters;
function FilterShout(const Value: TGingerValue;
const Args: TGingerValueArray; const KwNames: array of string;
const KwValues: TGingerValueArray): TGingerValue;
begin
Result := GingerStr(UpperCase(GingerToDisplayString(Value)) + '!!!');
end;
function TestIsTheAnswer(const Value: TGingerValue;
const Args: TGingerValueArray): Boolean;
begin
Result := GingerToDisplayString(Value) = '42';
end;
GingerRegisterFilter('shout', @FilterShout);
GingerRegisterTest('theanswer', @TestIsTheAnswer);Once registered, {{ "hello" | shout }} and {% if x is theanswer %}
work exactly like a built-in. A filter that needs to know whether
autoescape is on (or the active locale/titlecasing setting) registers
instead as a TGingerEvalCtxFilterFunc via
GingerRegisterEvalCtxFilter — see upper/lower/join in
GingerFilters.pas for worked examples of that signature.
Registration through GingerRegisterFilter/GingerRegisterEvalCtxFilter/
GingerRegisterTest is global and process-wide, not scoped to a single
Environment — call one of these once at startup, and it applies to
every Environment in that process, including ones created later. For
two Environments with two different filter/test sets, see
Env.RegisterFilter/RegisterEvalCtxFilter/RegisterTest below.
Per-Environment filters and tests
Real Jinja2's env.filters/env.tests are plain dicts scoped to one
Environment — two Environments in the same process can register
different filters, even though GingerRegisterFilter above cannot.
TGingerEnvironment has the matching per-instance calls:
Env.RegisterFilter('shout', @FilterShout);
Env.RegisterEvalCtxFilter('loud', @EvalCtxFilterLoud);
Env.RegisterTest('theanswer', @TestIsTheAnswer);These use the same TGingerFilterFunc/TGingerEvalCtxFilterFunc/
TGingerTestFunc types as the process-wide registration above. A
filter or test registered on Env shadows a process-wide one of the
same name (of either kind) for every render through that Env,
including one reached via {% include %} or {% import %} — but
two different Environments never see each other's registrations.
Registering the same name twice on the same Env overwrites the
earlier value, same as RegisterGlobal.
|map('name')/|select(...)/|reject(...)/|selectattr(...)/
|rejectattr(...)'s by-name filter/test dispatch also sees
Environment-registered filters/tests, same precedence as above (see
ADR-0053).
Custom globals
This is Ginger's equivalent of real Jinja2's Environment.globals —
a value (or callable) available in every template rendered through a
given TGingerEnvironment, without passing it as render data each
time:
Env.RegisterGlobal('appName', GingerStr('Ginger'));
Env.RegisterGlobal('now', GingerCallable(@CurrentTimestamp));{{ appName }} and {{ now() }} then work in any template rendered
through Env, including one reached via {% include %} or
{% import %}. Unlike GingerRegisterFilter/GingerRegisterTest
above, this is scoped to the one Environment — two Environments in
the same process can register different globals — because
TGingerEnvironment already owns exactly this kind of per-instance
configuration (TrimBlocks, DefaultAutoescape, …).
Two precedence rules, both matching real Jinja:
- A plain context variable of the same name always wins over a
registered global (
Ctx.SetVar('appName', ...), or data passed into render, beatsEnv.RegisterGlobal('appName', ...)). - A registered global can shadow one of the six hardcoded names
(
range/dict/cycler/joiner/namespace/lipsum) — real Jinja'senv.globalsis a plain dict those six live in too, and reassigning one of its keys overrides it the same way.
Registering the same name twice overwrites the earlier value, matching a dict's own semantics — there is no separate "unregister" call.
Integrating with a web framework: mORMot2 / Brook
Ginger renders a template to a string. It has no dependency on any
particular web framework, HTTP stack, or I/O model — which means
wiring it into mORMot2 or the Brook Framework is the same
small amount of glue either way: build a TGingerContext from the
request/route data, call Env.RenderTemplate, and write the result
to whatever response object that framework gives you.
No ready-made adapter package exists for either framework yet — this
is a conceptual sketch of the integration shape using Ginger's real,
existing public API, not a claim that GingerMormot/GingerBrook
units ship today.
mORMot2 sketch
A mORMot2 HTTP server method typically ends by setting a response body string. A Ginger-backed handler would look like:
function TMyServer.RenderHello(Ctx: THttpServerRequest): Cardinal;
var
TplCtx: TGingerContext;
begin
TplCtx := TGingerContext.Create;
try
TplCtx.SetVar('name', GingerStr(Ctx.InURI)); // whatever your route data is
Ctx.OutContent := GingerEnv.RenderTemplate('hello.html', TplCtx);
Ctx.OutContentType := 'text/html; charset=utf-8';
Result := 200;
finally
TplCtx.Free;
end;
end;GingerEnv here is a single, long-lived TGingerEnvironment created
once at server startup (its template cache is why: creating it per
request would reparse every template on every hit).
Brook Framework sketch
Brook's request handlers typically write directly to a response stream/writer. The same pattern applies — build a context, render, write the string:
procedure TMyHelloAction.Get;
var
TplCtx: TGingerContext;
begin
TplCtx := TGingerContext.Create;
try
TplCtx.SetVar('name', GingerStr(Request.Params.Values['name']));
Response.ContentType := 'text/html; charset=utf-8';
Response.Write(GingerEnv.RenderTemplate('hello.html', TplCtx));
finally
TplCtx.Free;
end;
end;In both cases, the integration point is exactly one call —
Env.RenderTemplate(Name, Ctx) — so adapting Ginger to a framework
you already use is mostly a question of where that framework wants
you to read inbound request data from, and where it wants the
rendered string written to. If there's real appetite for a
ready-made adapter unit for either framework, that's a natural
follow-up contribution — see docs/adr/ for the project's design
process if you're considering one.
Authorship / provenance
Most of this codebase — lexer, parser, AST, evaluator, renderer,
filters, the test suite, and every ADR under docs/adr/ — was written
by Claude (Anthropic) as my coding agent. I set scope, made the design
decisions it surfaced, reviewed and ground-truthed behavior against
real Jinja2, and decided what shipped. Not hand-written with AI
polish; substantially AI-generated under continuous human direction
and review. docs/adr/ is the dated record of that process.
Project layout
src/— the engine itself, one Pascal unit per pipeline stage (lexer → parser → AST → evaluator → renderer), plusGingerLoader(template sources) andGingerLib.lpk(the installable package).tests/— the full automated test suite.docs/adr/— Architecture Decision Records: every non-trivial design choice, ground-truthed against real Jinja2 where applicable, numbered and dated.

