An open API service indexing awesome lists of open source software.

https://github.com/yzfly/gojinja2

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

conformance-testing go golang jinja jinja2 jinja2-templates python template-engine templates templating

Last synced: 24 days ago
JSON representation

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

Awesome Lists containing this project

README

          

# ⛩️ gojinja2

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

[![CI](https://github.com/yzfly/gojinja2/actions/workflows/ci.yml/badge.svg)](https://github.com/yzfly/gojinja2/actions/workflows/ci.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/yzfly/gojinja2.svg)](https://pkg.go.dev/github.com/yzfly/gojinja2)
[![Go Version](https://img.shields.io/badge/go-%3E%3D1.22-00ADD8?logo=go)](https://go.dev)
[![Conformance](https://img.shields.io/badge/CPython%20conformance-557%2F557-brightgreen)](#how-conformance-is-verified)
[![License: CC BY-NC 4.0](https://img.shields.io/badge/license-CC%20BY--NC%204.0-lightgrey)](LICENSE)

English | [简体中文](README.zh-CN.md)

---

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 statement** — `if` / `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` & `import`** — `ignore 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-exact** — `trim_blocks`, `lstrip_blocks`, `{%-`, `{%+`, line statements & line comments, custom delimiters (PHP / ERB styles)
- **Extensions** — `do`, `loopcontrols` (`break` / `continue`), `i18n` (`trans` / `pluralize` / `trimmed` / gettext context)
- **Loaders** — `DictLoader`, `FileSystemLoader`, `FSLoader` (works with `embed.FS`), `FunctionLoader`, `ChoiceLoader`, `PrefixLoader`

## Quick start

```bash
go get github.com/yzfly/gojinja2
```

```go
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": ""})
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):

```bash
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 declaration** — `sandbox`, `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](CONTRIBUTING.md). The golden rule: any behavioral claim must come with a CPython-generated fixture.

## License

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

## Author

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