- Zig 98.2%
- Nix 1%
- Python 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Zig 0.17.0 from nixpkgs replaces the devshell's patched 0.16 -- its test runner is fixed, so the fuzz targets compile under --fuzz as they are, and the fuzz test binary is built with LLVM so that the fuzzer gets coverage. Array repetition with ** is gone from the language; the two tests that used it build their strings with @splat. Version 0.2.0; 0.16 lives on the zig-0.16 branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwwDvnsTr5ah1n3cntWbmK |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-jinja
A subset of Jinja for Zig 0.17: the template language Home Assistant speaks, parsed once and rendered at run time over data a host supplies.
const jinja = @import("jinja");
var d: jinja.Diagnostic = .{};
var t = try jinja.Template.parse(gpa, "{{ (temp|float * 9 / 5 + 32)|round(1) }}°F", .{}, &d);
defer t.deinit();
const out = try t.renderAlloc(arena, &env, &d); // "70.7°F"
The API documentation is generated from the doc comments and published at https://jeff.jcollie.page/zig-jinja/.
It exists for templates that somebody other than the program's author writes — a key on a Stream Deck, a label in a configuration file — and three properties follow from that:
- A syntax error is an error. It comes back with a line and column and nothing is rendered. There is no recovery that skips a broken statement and renders the rest, which is the one outcome worse than refusing.
- Every render is bounded — bytes written, loop iterations, recursion
depth, list lengths — by
Limits, whatever the template says. A template cannot make a render take more than its host allowed. - Names can be checked when parsing. A host that lists the variables,
functions and filters it provides gets
{{ stats('light.x') }}refused at load time, with a position, instead of a template that quietly renders nothing.
There is no I/O anywhere in it: no include, import or extends, no
loaders, no clock. A template can read what its host hands it and write its
output, and nothing else.
What it speaks
| Tags | {{ expr }}, {% statement %}, {# comment #}, whitespace control with - on either side, {% raw %} |
| Statements | if / elif / else; for with unpacking, a filter (for x in xs if cond), else, and loop.index, index0, revindex, revindex0, first, last, length, previtem, nextitem; set, including set a, b = …, set ns.attr = … and {% set x %}…{% endset %} |
| Literals | integers, floats, strings with Python's escapes, true/false/none in either case, lists, tuples, dicts |
| Operators | + - * / // % **, ~, == != < <= > >= chained as in Python, in and not in, and/or/not returning operands as Python's do, a if c else b |
| Access | x.attr, x['key'], x[0], x[-1], x.0, slices x[a:b:c] |
| Filters | abs capitalize count default/d escape/e first float format int join last length list lower map max min reject rejectattr replace reverse round safe select selectattr sort string sum title tojson trim truncate unique upper |
| Tests | boolean defined divisibleby eq/equalto even false float ge gt in integer iterable le lower lt mapping ne none number odd sameas sequence string true undefined upper |
| Methods | on strings upper lower title capitalize strip lstrip rstrip startswith endswith split replace join find count isdigit; on dicts items keys values get |
| Functions | range, dict, namespace |
Values follow Python rather than Zig, because that is what a Jinja template
means: {{ 1 / 2 }} is 0.5, {{ none }} is None, {{ 3.0 }} is 3.0,
{{ 2.675|round(2) }} is 2.67 — rounded from the exact binary value, a tie
to even, as Python does — and '%5.1f'|format(x) and '%d%%' % pct format
the way printf does. An undefined name renders as nothing and is false, and
doing anything else with it is an error, which is Jinja's default.
What it is narrower about, deliberately: integers are 64 bits, and overflow is
an error rather than a big integer; case conversion is ASCII; a tuple is a
list, and prints as one; format is printf's conversions and not
str.format; macros, call, with, loop.cycle and recursive loops are
not there.
Using it from a host
A host provides an Env:
const env: jinja.Env = .{
.context = &my_state, // handed to every callback
.resolve = resolveName, // looks up a free variable
.functions = &.{.{ .name = "states", .call = states }},
.filters = &.{.{ .name = "duration", .call = duration }},
.autoescape = true, // for XML or HTML output
.limits = .{ .max_output = 16 << 10 },
};
A callback is handed a *Call, which carries the render's arena, the host's
context, and the place in the template it was called from, so that
call.fail("no entity '{s}'", .{id}) reports the error where the template
wrote it.
Data can be handed over eagerly — Value.list, Value.map, or a parsed
JSON document through json.fromJson, which borrows its strings — or lazily,
as an Object whose callbacks answer attribute lookups and method calls when
the template asks. The second is what keeps a render from copying everything
a template might read.
Template.dependencies walks a parsed template and says what it reads: the
free names, the attribute paths read off them (player.position), and every
function call with its string-literal arguments, so
states('light.kitchen') names the entity outright. A host that redraws on
change uses that to know what to watch without running anything.
isTemplate(text) says whether a string contains template syntax at all, so a
setting can be plain text or a template and plain text stays exactly as it
was.
Checking it against Jinja
tests/conformance.json is a list of templates, each with the variables it is
rendered with and what Jinja renders it as — or that Jinja refuses it. The
expectations are not written by hand:
$ nix develop -c python3 tools/jinja_oracle.py # rewrite them from Jinja2
$ nix develop -c python3 tools/jinja_oracle.py --check # only say which differ
renders every case with Jinja2 and writes down what it got, and
zig build test holds this library to it. A case added there is a claim about
Jinja, checked against Jinja.
Development
$ nix develop
$ zig build test --summary all
$ zig build fuzz --fuzz # until interrupted
$ zig build fuzz-run -- --seconds 300
$ zig build docs && zig build docs-serve # then http://localhost:8000
The targets in tests/fuzz.zig say that any bytes as a template must parse
or fail at a position inside the source, and that any template must render
within its limits or fail with one of its own errors. zig build fuzz --fuzz
runs them under Zig's own fuzzer, with coverage feedback; zig build fuzz-run
drives them with a loop of its own whose runs are reproducible from a seed,
which is what the workflow uses. tools/fuzz.zig says more.
Zig 0.16 users want the zig-0.16 branch, or the v0.1.0 tag.
Where this lives
The canonical repository is on my Forgejo instance, and Tangled carries a mirror of it:
git clone https://git.jcollie.dev/jeff/zig-jinja.git
It is also on Radicle as
rad:z2GvXg4i7YQjJFXBntCqxeb73huUt, which is the only name that finds it
there — a peer-to-peer repository has no host to browse:
rad clone rad:z2GvXg4i7YQjJFXBntCqxeb73huUt
All three serve the same history. CI, the published documentation and the issue tracker follow the canonical one.
To depend on it:
$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-jinja.git
Licence
MIT. The project follows the REUSE specification;
reuse lint checks it.
References cited
- Pallets. Template Designer Documentation. Jinja 3.1. https://jinja.palletsprojects.com/en/stable/templates/. The language: its syntax, operators, filters, tests and scoping.
- Pallets. Jinja source code,
src/jinja2/lexer.pyandparser.py. https://github.com/pallets/jinja. The precedence of the expression grammar and the handling of whitespace control. - Home Assistant. Templating. https://www.home-assistant.io/docs/configuration/templating/. The templates this library is written to run.
- Python Software Foundation. Built-in Types: printf-style String
Formatting. https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting.
formatand the%operator. - Python Software Foundation. Built-in Functions: round(). https://docs.python.org/3/library/functions.html#round. Rounding a tie to even, from the exact value.