Metadata-Version: 2.4
Name: templore
Version: 1.0.0
Summary: A from-scratch, zero-dependency Jinja2-like template engine in pure Python
Author: EQUINOX
License: MIT
Keywords: template,jinja,templating,renderer,from-scratch,zero-dependency
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Text Processing :: Markup
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# templore

A from-scratch, **zero-dependency** Jinja2-like template engine in pure Python.
Written to actually understand how a real template engine works end to end:
lexing → expression parsing → AST → tree-walking rendering → inheritance.

No `pip install` of anything — the whole engine is standard-library only.

```python
from templore import Environment, DictLoader

env = Environment(loader=DictLoader({"hi": "Hello {{ name|upper }}!"}))
print(env.get_template("hi").render(name="ada"))     # Hello ADA!

print(Environment().from_string("{{ 1 + 2 * 3 }}").render())  # 7
```

## Features

| Category | What's supported |
| --- | --- |
| Output | `{{ expr }}` with attribute (`a.b`), index (`a[0]`), slices (`a[1:3]`, `a[::-1]`), calls (`f(x)`) |
| Operators | `+ - * / // % ~`, `== != < > <= >=`, `and or not`, `in` / `not in` |
| Literals | ints, floats, strings, `true/false/none`, lists `[..]`, dicts `{..}` |
| Comprehensions | `[x*2 for x in xs if x]`, `{k: v for k, v in pairs}` (scoped, both backends) |
| Control flow | `{% if/elif/else %}`, `{% for %}` (+ `{% else %}`, tuple unpacking) |
| Inline conditional | `{{ a if cond else b }}` (else optional) |
| Loop variable | `loop.index/index0/first/last/revindex/length/previtem/nextitem/cycle()` |
| Assignment | `{% set x = expr %}`, attribute/item targets (`{% set ns.x = ... %}`), block set (`{% set x %}...{% endset %}`, optionally filtered) |
| Mutable state | `namespace(...)` for values that survive across loop iterations |
| Side effects | `{% do list.append(x) %}` |
| Scoping | `{% with x = a, y = b %}...{% endwith %}` |
| Macros | `{% macro f(a, b=default) %}...{% endmacro %}`, called `{{ f(x) }}` (closes over scope) |
| Call blocks | `{% call f(x) %}body{% endcall %}` — macro renders the body via `caller()` |
| Imports | `{% import 'lib' as ns %}`, `{% from 'lib' import a, b as c %}` (reuse macros) |
| Filters | `\|` pipeline with args & chaining: `{{ x\|round(2)\|string }}` — 40+ built-ins |
| Filter blocks | `{% filter upper %}...{% endfilter %}` |
| Tests | `{% if x is defined %}`, `is not none`, `is even`, `is divisibleby(3)`, `is sameas(y)`, … |
| Comments | `{# ... #}` |
| Line statements | opt-in `# for x in xs` / `## comment` via `Environment(line_statement_prefix=...)` |
| Raw | `{% raw %}...{% endraw %}` (emit template syntax literally) |
| Whitespace control | `{%- ... -%}` / `{{- ... -}}` |
| Inheritance | `{% extends %}`, `{% block %}` (multi-level), `{{ super() }}` |
| Includes | `{% include %}` (shares the caller's context) |
| Autoescaping | opt-in HTML escaping with `\|safe` / `\|escape` and `Markup`; `{% autoescape true/false %}` block toggle |
| Undefined | lenient (renders `""`) or `strict` mode |
| Error locations | runtime errors carry the template name + line number (both backends) |
| Custom delimiters | swap `{{ }}` / `{% %}` / `{# #}` for anything via `Environment(delimiters=...)` |
| Two backends | tree-walking interpreter (default) or `Environment(compiled=True)` codegen (~2x faster) |

## Install / run

```bash
python examples/demo.py       # guided tour
python examples/benchmark.py  # interpreter vs compiled
python -m pytest -q           # 251 tests

# render a template file from the command line:
python -m templore render page.html --data ctx.json
python -m templore render page.html --var name=Ada --compiled -o out.html
```

## API

```python
Environment(loader=None, autoescape=False, undefined="lenient")
    .from_string(source, name=None) -> Template
    .get_template(name) -> Template          # via loader, cached

Template.render(context=None, **kwargs) -> str
```

Loaders: `DictLoader({name: source})`, `FileSystemLoader(path_or_paths)`.

### CLI

```
python -m templore render TEMPLATE [--data FILE.json] [--var K=V ...]
                                   [--compiled] [--autoescape] [-o OUT]
```

The template is loaded via a `FileSystemLoader` rooted at its own directory, so
`extends` / `include` / `import` resolve relative to it. `--var` values are
strings and override keys from `--data`.

## How it works

The pipeline lives in `templore/` as five small stages:

```
source ──Lexer──▶ tokens ──Parser──▶ AST ──Template.render──▶ output
                              │
                     ExpressionParser (recursive descent w/ precedence)
```

- **`lexer.py`** — splits raw source on the `{{ }}` / `{% %}` / `{# #}`
  delimiters (applying `-` whitespace trimming), then tokenizes the expression
  text inside each tag.
- **`expressions.py`** — a recursive-descent parser that turns expression tokens
  into `Expr` nodes, honoring operator precedence (`or < and < not < compare <
  add < mul < unary < filter < postfix < primary`).
- **`parser.py`** — assembles statement nodes (`for`/`if`/`set`/`block`/…),
  recursing through nested bodies until it meets the matching end tag.
- **`nodes.py`** — the AST. Expression nodes `evaluate(ctx, env)`; statement
  nodes `render(ctx, env)`. A plain **tree-walking interpreter** — no codegen.
- **`environment.py`** — configuration, the template cache, and the inheritance
  engine. It flattens the `extends` chain into per-block "chains" so that
  overriding and `super()` fall out naturally.

Runtime bits (`runtime.py`): a scoped `Context`, the lenient `Undefined`
sentinel, the `Namespace` for imports, and the `Markup` safe-string used by
autoescaping. Filters and tests live in `filters.py`.

### The compiled backend (`compiler.py`)

`Environment(compiled=True)` swaps the tree-walker for a code generator: the AST
is turned into Python *source* for a `root(ctx, env)` function, `exec`'d once,
and cached on the template. Expressions become inlined Python expressions and
`{% for %}` / `{% if %}` become native loops/branches — no per-node dispatch at
render time. Inspect it with `template.compiled_root.__source__`.

```python
env = Environment(compiled=True)
t = env.from_string("{% for x in xs %}{{ x|upper }}{% endfor %}")
print(t.compiled_root.__source__)   # the generated Python
```

Templates using `extends` / `block` / `macro` / `import` / `call` are left to
the interpreter (the compiler returns `None` and rendering falls back), so
turning `compiled=True` on is always safe — output is identical either way,
which the test suite verifies case-by-case.

## License

MIT
