Skip to content

yzfly/gojinja2

Repository files navigation

⛩️ gojinja2

A 100% Jinja2-compatible template engine for Go — verified character-by-character against CPython.

CI Go Reference Go Version Conformance License: CC BY-NC 4.0

English | 简体中文


Existing Go ports of Jinja2 (pongo2, gonja, …) stop at being template parsers. But Jinja2 is not a syntax — it is a language that lives on top of the Python runtime. {{ -7 // 2 }}, {{ 1 == True }}, {{ "日本語"[1:] }}, {{ d.items() }}: getting these right means reimplementing a thin slice of Python's object model, not just its template grammar.

gojinja2 implements that semantic layer, and proves it the only way that means anything: every test fixture is generated by running the official CPython Jinja2 (pallets/jinja 3.1.6) and recording its real output. The Go engine must match it character for character — rendered output, token streams, AST shapes, and even error messages.

Highlights

  • Full expression grammar — chained comparisons, Python slicing, *args / **kwargs, conditional expressions, filter/test expressions, implicit tuples
  • Every statementif / for (incl. recursive, the complete loop object, loop filtering) / set (incl. block assignments and namespace()) / with / macro (caller, varargs, kwargs, closures, call-time defaults) / call / filter / autoescape
  • Template inheritance, completely — multi-level extends, block (scoped / required), super() and super.super(), self, dynamic parent expressions
  • include & importignore missing, list fallbacks, with / without context
  • All 54 built-in filters, 39 tests, every global (range, dict, namespace, cycler, joiner, lipsum) — including the long tail: tojson, urlize, wordwrap (a faithful textwrap port), truncate with leeway, groupby with case restoration
  • Python value semantics — banker's rounding, floor division and modulo sign rules, 1 == 1.0 == True, rune-level string indexing, insertion-ordered dicts with Python key unification
  • The four Undefined types — default / Chainable / Debug / Strict, full behavior matrix
  • Autoescape & Markup contagion — through ~, +, %, join, replace, every escape path
  • Whitespace control, token-exacttrim_blocks, lstrip_blocks, {%-, {%+, line statements & line comments, custom delimiters (PHP / ERB styles)
  • Extensionsdo, loopcontrols (break / continue), i18n (trans / pluralize / trimmed / gettext context)
  • LoadersDictLoader, FileSystemLoader, FSLoader (works with embed.FS), FunctionLoader, ChoiceLoader, PrefixLoader

Quick start

go get github.com/yzfly/gojinja2
package main

import (
    "embed"
    "fmt"

    gojinja2 "github.com/yzfly/gojinja2"
)

//go:embed templates
var templates embed.FS

func main() {
    // Inline templates
    env := gojinja2.NewEnvironment()
    tpl, _ := env.FromString("Hello {{ name|title }}! {% for i in range(3) %}{{ i }}{% endfor %}")
    out, _ := tpl.Render(map[string]any{"name": "world"})
    fmt.Println(out) // Hello World! 012

    // File templates + inheritance + autoescaping
    env2 := gojinja2.NewEnvironment()
    env2.Loader = gojinja2.NewFSLoader(templates, "templates")
    env2.Autoescape = true
    page, _ := env2.GetTemplate("child.html")
    html, _ := page.Render(map[string]any{"user": "<script>"})
    fmt.Println(html) // <script> is escaped
}

Mapping Go values

Attribute access follows Python semantics (attribute first, then subscript):

  • map[string]any and nested structures behave as dicts — key access, .items(), .get() and friends all work
  • Struct fields and methods resolve by exact name first, then snake_case → CamelCase ({{ user.get_name() }} calls GetName())
  • Numbers normalize to int64 / float64; slices and arrays behave as lists

How conformance is verified

No hand-written expectations. Generators under tools/ feed each case (template × context × environment config) to the official CPython Jinja2 and record what it actually produces — output or exception. The Go test suites then align, character by character:

Layer Corpus What must match
Lexer 114 cases / 632 tokens token types, values, line numbers; error messages verbatim
Parser 110 cases AST equal to Python repr(ast) character-for-character; 18 error cases
Render 333 cases rendered output verbatim; runtime error messages verbatim

Current score: 557/557, plus one documented divergence (see below).

Regenerate the corpus yourself (requires CPython and the reference checkout):

git clone --depth 1 --branch 3.1.6 https://github.com/pallets/jinja.git reference/jinja
PYTHONPATH=reference/jinja/src python3 tools/gen_lexer_fixtures.py  > lexer/testdata/lexer_fixtures.json
PYTHONPATH=reference/jinja/src python3 tools/gen_parser_fixtures.py > parser/testdata/parser_fixtures.json
PYTHONPATH=reference/jinja/src python3 tools/gen_render_fixtures.py > rendertest/testdata/render_fixtures.json
go test ./...

This pipeline is the project's real asset: adding coverage is mechanical — add a case, CPython produces the expectation, the Go suite enforces it.

Documented divergences

Honesty over marketing. The complete list:

  1. Integer precision — Python integers are arbitrary-precision; gojinja2 uses int64 (a big.Int upgrade path is reserved). Literals beyond int64 raise an error rather than silently truncating.
  2. Native Go map iteration order — user-supplied Go maps iterate in sorted key order (Go maps are unordered by design). Dicts created inside templates are strictly insertion-ordered, matching Python.
  3. Out of scope by declarationsandbox, async, bytecode caching: Python-ecosystem mechanisms with no Go equivalent. Not counted against compatibility.

Architecture

gojinja2/
├── lexer/        # hand-written scanner replicating the official regex semantics, lazy errors
├── parser/       # 1:1 port of parser.py
├── nodes/        # AST, with Python-repr-aligned dumps for conformance
├── runtime/      # the Python semantic layer: values, operators, Undefined, Markup, ordered dict
├── exceptions/   # error types mirroring jinja2.exceptions
├── rendertest/   # render-level conformance suite
├── tools/        # corpus generators (CPython as ground truth)
└── *.go          # Environment / Template / interpreter / filters / loaders / extensions

Two deliberate departures from CPython's implementation, neither observable from templates:

  • Jinja2 compiles templates to Python source; gojinja2 uses a tree-walking interpreter (Go cannot exec, and the conformance suite proves behavioral equivalence).
  • The official lexer is regex-driven with lookbehind/lookahead, which Go's RE2 cannot express; the scanner is hand-written to replicate those semantics exactly — including lazy error ordering.

Contributing

See CONTRIBUTING.md. The golden rule: any behavioral claim must come with a CPython-generated fixture.

License

CC BY-NC 4.0 (non-commercial). For commercial licensing, contact the author.

Author

云中江树 (yzfly) — WeChat Official Account: 云中江树

About

⛩️ 100% Jinja2-compatible template engine for Go — verified character-by-character against CPython | Go 语言 Jinja2 模板引擎, 与 CPython 逐字符对齐验证

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors