Metadata-Version: 2.1
Name: flextex
Version: 1.8.1
Summary: Lightweight template engine with branching, loops, macros, and expressions.
Author-email: Anton Petersen <quarkatron@gmail.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Dist: parsek
Requires-Dist: pytest ; extra == "test"
Requires-Dist: coverage ; extra == "test"
Requires-Dist: drawch ; extra == "test"
Project-URL: Home, https://github.com/anptrs/flextex
Provides-Extra: test

# FlexTex

[![PyPI version](https://badge.fury.io/py/flextex.svg)](https://pypi.org/project/flextex/)
[![License](https://img.shields.io/pypi/l/flextex.svg)](https://opensource.org/license/mit/)
[![Python versions](https://img.shields.io/pypi/pyversions/flextex.svg)](https://pypi.org/project/flextex/)

Pure Python, lightweight, extensively tested, full-featured text template library. Supports conditional logic, lists, expressions, macros, and more. Well suited for dynamic prompts for AI agents, IoT web pages, and general template use, where templates may need to be dynamically composed, include conditional sections, loops, macros, lists and more.

- Tight single-file core with a single dependency ([parsek](https://github.com/anptrs/parsek), a tiny, single-file pure-Python parser).
- Simple API: for most use cases, a single `evaluate()` function call is enough.
- Low impact template syntax: only directives `{% ... %}` and comments `{# ... #}`
- Expressions and Python statements in templates
- Control flow: `if` / `elif` / `else`, `foreach`
- Lists: inline, named, nested; built-in `ol(...)` / `ul(...)`
- Macros with arguments
- Imports from files, packages/resources, or substitutions
- Helpful error messages with source/position and import-trace context

## Install

```sh
pip install flextex
```

## Hello, World

```python
from flextex import evaluate

print(evaluate("Hello, {% name %}!", {"name": "World"}))
# -> Hello, World!
```
Or from command line:
```sh
python -m flextex -e "Hello, {% name %}\!" <<< '{"name": "World"}'
```

## Table of Contents

- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Directives](#directives)
  - [Quick Reference](#directives)
  - [Substitutions and Expressions](#substitutions-and-expressions)
  - [Template Variables: `var`](#template-variables-var)
  - [Macros: `def`](#macros)
  - [Imports](#imports)
  - [Flow Control: `if` / `elif` / `else`](#flow-control-if--elif--else)
  - [Loops: `foreach`](#foreach)
  - [Lists](#lists)
- [Built-in Functions](#built-in-functions-available-in-expressions)
- [Python API Reference](#python-api-reference)
- [Error Reporting](#error-reporting)
- [Command Line Interface](#cli-reference)
- [Performance & Thread Safety](#performance-notes)
- [Security Notes](#security-notes)
- [Development & License](#development--contributing)


## Quick Start

```python
from flextex import evaluate

PROMPT_TEMPLATE = """\
# IDENTITY
You are a Friendly Assistant. Your job is to engage in friendly and
helpful conversations with the USER.

# CONTEXT
User name: {% user_name %}
User's age: {% user_age %}
User's hobbies: {% user_hobbies %}
{% if chat_history %}
    Chat history:
      {% chat_history %}
{% endif %}
  ...
"""

chat_history = [
    "[ASSISTANT] Hello! How can I help you today?"
]

prompt = evaluate(PROMPT_TEMPLATE, {
    "user_name": "Bob",
    "user_age": 107,
    "user_hobbies": ["spear fishing", "skydiving", "beach"],
    "chat_history": chat_history
})
```
The rendered `prompt` will look like this:
```plaintext
# IDENTITY
You are a Friendly Assistant. Your job is to engage in friendly and
helpful conversations with the USER.

# CONTEXT
User name: Bob
User's age: 107
User's hobbies: spear fishing, skydiving, and beach.
Chat history:
  [ASSISTANT] Hello! How can I help you today?
```

> ❇️ You can precompile once and evaluate many times:
>
> ```python
> from flextex import precompile, evaluate
>
> prompt_ast = precompile(PROMPT_TEMPLATE) # precompile once
> while chatting:
>     ...
>     prompt = evaluate(prompt_ast, prompt_substitutions) # fast subsequent evaluations
>     ...
> ```

## Core Concepts

FlexTex uses only two syntax elements: the `{% ... %}` directive and the `{# ... #}` comment (comments are removed from the final output). This minimal syntax ensures that your source text can be in any format — Markdown, HTML, plain text, or anything else — without interference.

- Directives (`{% ... %}`) and comments (`{# ... #}`) can appear anywhere.
- A directive may span multiple lines.
- Multiple directives can be on a single line.

> ➰ If you need to escape an opening `{`, use `{{`. This is rarely necessary, since only the full `{%` or `{#` sequences are treated as special.<br>
> ➰ `}}` is accepted for symmetry yielding `}`. A single `}` is literal unless forming `%}` or `#}`.

### Whitespace and Indentation

Careful consideration was given to how FlexTex handles indentation - treating it as __intent__. Structural indentation is removed from output, but deliberate indentation is preserved. This allows you to clearly see the conditional flow in your template source while keeping the intended output layout.

- Indentation and new lines have _no semantic meaning_ (unlike Python); they are only for readability or output formatting.
- The default _structural indentation_ (e.g., inside `if` or `foreach` blocks) is 4 spaces ([configurable](#set_indent)). It's optional but helps readability, and is __removed__ from final output.
- Lines within a directive block are dedented to the outer block level + any additional structural indentation. Any surplus indentation is kept.
- To preserve indentation in output (using default 4 here as an example):
  - indent less than 4 spaces at the block level (keeps those spaces), or
  - indent more than 4 spaces to keep the surplus beyond the structural 4.
- Trailing whitespace on lines is removed.
- Tabs are ignored for structural indentation (use spaces).
- Only `\n` line endings are interpreted for structural indentation.

In short: FlexTex keeps templates readable while preserving your intended layout in the output. __Structural indentation is removed - intentional indentation stays__.

Examples:

<table><tr><th>Input Template</th><th>Output</th><th>Notes</th></tr><tr><td>

```jinja
Hello
{% if show %}
    Inner
      Still inner
{% endif %}
World
```

</td><td>

```text
Hello
Inner
  Still inner
World
```

</td><td>
when `show = True`
</td></tr><tr><td>

```jinja
My To-Do List:
{% foreach items %}
  - {% item %}
{% /foreach %}

No indent:
{% foreach items %}
    - {% item %}
{% /foreach %}

2-space indent:
{% foreach items %}
      - {% item %}
{% /foreach %}

4-space indent:
  {% foreach items %}
    - {% item %}
  {% /foreach %}
```

</td><td>

```text
My To-Do List:
  - First
  - Second

No indent:
- First
- Second

2-space indent:
  - First
  - Second

4-space indent:
    - First
    - Second
```

</td><td>
bullet indent preserved (2-space indent)<br>since item is at less than structural indent (4 spaces)<br>
<br><br>bullet indent removed completely<br>since item is at exactly structural indent (4 spaces)<br>
<br><br>bullet indent is at 2 spaces<br>since item is 2 spaces more than structural indent (4 spaces)<br>
<br><br>the whole block is indented 2 spaces.<br>and items are at 2-space indent<br>
</td></tr></table>


## Directives

Quick reference of all available directives `{% ... %}` in FlexTex templates:

<small>

| Directive | Purpose | Notes |
|-----------|---------|-------|
| `{% expr %}` | [Substitution](#substitutions-and-expressions) | Identifier, expression, or statement(s) (`;` separated) |
| `{% var name %}`<br>`{% var name = initializer %}` | [Template variables](#template-variables-var) | Only vars created via `var` are mutable |
| `{% def macro(args) %}`<br>&nbsp;&nbsp;&nbsp;&nbsp;. . .<br>`{% /def %}` | [Define macro](#macros) | Call via `{% macro(arg1, x=2) %}` |
| `{% <source> %}` | [Import / include](#imports) | `source` can be path, package<br>resource, or a substitution name |
| `{% if cond %}`<br>`{% elif cond %}`<br>`{% else %}`<br>`{% endif %}` or `{% /if %}` | [Conditional branching](#flow-control-if--elif--else) | `cond` can be identifier or expression |
| `{% foreach items %}`<br>&nbsp;&nbsp;&nbsp;&nbsp;. . .<br>`{% /foreach %}` | [Loop over iterable](#foreach) | Alias: `for`<br> Loop vars: `item`, `i` (0‑based), `I` (1‑based),<br>`count`, `is_last` |
| `{% list name %}`<br>&nbsp;&nbsp;&nbsp;&nbsp;. . .<br>`{% /list %}` | [Define a reusable list](#lists) | Use later anywhere a list is expected |
| `{% list %}`<br>&nbsp;&nbsp;&nbsp;&nbsp;. . .<br>`{% /list %}` | [Inline list render](#render-in-place-list) | Renders immediately (like `<ul>/<ol>`) |
| `{% li %} ... {% /li %}` | [List item](#lists) | Closing tag `{% /li %}` is optional |
| `{% meta key=value %}` | [Template metadata](#template-metadata---meta) | Set template-level options, e.g. `indent=2`|

</small>

In addition to the above directives, the following [utility functions](#built-in-functions-available-in-expressions) are available in all expressions by default:
`alpha`, `ordinal`, `roman`, `cardinal`, [`ol`](#ol_ul), [`ul`](#ol_ul), [`en_and`](#en_and_or), [`en_comma_and`](#en_and_or), [`en_or`](#en_and_or), [`en_comma_or`](#en_and_or), `is_list`, `is_nested_list`, [`outer`](#outer).

---
### Substitutions and Expressions
Substitution is any directive `{% ... %}` that is not a control or data structure. The result of the substitution is inserted into the output text.
```jinja
{% user_name %}     {# simple substitution #}
{% user_age + 1 %}  {# expression evaluation #}
{% user_hobbies | join(", ") %}  {# list to string conversion using Python features #}
{% f"i:02" %}       {# Python f-string (assuming i is provided in subs or is a var) #}
{% x += 1 %}        {# increment a variable; x must be defined before hand: #}
                    {# e.g., with {% var x = 0 %} #}
```

- You can use any Python expression or statement (statements must be single-line; use `;` to separate multiple statements).
- Statements evaluate to `None` and render as empty string.
- Callables in substitutions are invoked automatically and their return value is rendered.
- Non-strings that are iterable render as inline lists in span contexts, or as block lists otherwise.
- Names used in substitutions/expressions can be provided by your host code via the `subs` argument to `evaluate()`. They can be:
  - Simple values (strings, numbers, booleans)
  - Callables (auto-invoked for simple substitutions and their return value is rendered)
  - Iterables (auto-rendered as inline lists in span contexts, or block lists otherwise)
  - Mappings with `header`/`items` for nested lists
  - Modules/classes (for use in expressions) _(requires eval enabled)_
  - Any object since attributes/methods can be accessed in expressions _(requires eval enabled)_

Providing substitutions:
```python
from flextex import evaluate

tmpl = "Hello, {% name %}! You turn {% age + 1 %} tomorrow."
print(evaluate(tmpl, {"name": "Alice", "age": 41}))
# -> Hello, Alice! You turn 42 tomorrow.
```

Layered substitutions (tuple precedence):
```python
from flextex import evaluate

app = {"app_name": "FlexBoard"}
user = {"name": "Bob"}

tmpl = "Welcome to {% app_name %}, {% name %}!"
print(evaluate(tmpl, (user, app)))  # `user` overrides keys in `app` if duplicated
# -> Welcome to FlexBoard, Bob!
```

Callables (auto-invoked vs. expressions):
```python
from datetime import datetime
from flextex import evaluate

def now_iso():
    return datetime.now().isoformat(timespec="seconds")

print(evaluate("Generated at {% now_iso %}", {"now_iso": now_iso}))
# -> Generated at 2004-09-04T12:34:56

# the following relies on eval being enabled:
print(evaluate("Generated at {% now_iso() %}", {"now_iso": now_iso}))
# -> Generated at 2004-09-04T12:34:56
```

Lists (inline vs. block):
```python
from flextex import evaluate

tmpl = "Hobbies: {% hobbies %} (span)\nBlock:\n{% hobbies %}"
print(evaluate(tmpl, {"hobbies": ["sailing", "skiing", "chess"]}))
# -> Hobbies: sailing, skiing, and chess (span)
# -> Block:
# -> sailing
# -> skiing
# -> chess
```

Providing modules/classes for expressions:
```python
from datetime import datetime, timezone
from flextex import evaluate

tmpl = "UTC now: {% datetime.now(timezone.utc).isoformat(timespec='seconds') %}"
print(evaluate(tmpl, {"datetime": datetime, "timezone": timezone}))
```

Safe mode (disable Python eval, still allow identifiers and callables):
```python
from flextex import precompile, evaluate

def now_iso():
    # still works in noeval mode because it's a simple identifier substitution
    from datetime import datetime
    return datetime.now().isoformat(timespec="seconds")

ast = precompile("Now: {% now_iso %}", noeval=True)
print(evaluate(ast, {"now_iso": now_iso}))  # OK

# Expressions will fail in noeval mode:
# precompile("Now: {% now_iso() %}", noeval=True)  -> FlextexError
```

---
### Template variables: `var`

Your templates can define variables with the `{% var ... %}` directive. Only variables created via `{% var ... %}` are mutable in later expressions/statements. Variables are strongly scoped to the enclosing block (unlike Python's scoping). A variable defined inside a `def`, `if`, `foreach`, or any other block is not visible outside that block, but is visible in nested inner blocks. A variable with the same name in the inner scope 'hides' the outer one. Use [`outer(var_name, level=1)`](#outer) to access an outer-scope variable with the same name from an inner block (e.g., in nested foreach). Of course, you can just copy the outer variable to a new inner variable with a different name.


```jinja
{% var x = 0 %}     {# define a mutable variable x with initializer expression 0 #}
{% x += 1 %}        {# increments x by 1, outputs nothing, var is now 1 #}
{% x %}             {# simple substitution, outputs 1 #}
{% (x := x + 1) %}  {# increments x by 1 and outputs 2 #}
{% x %}             {# also outputs 2 #}

{% var l = ['Alice', 'Bob'] %}  {# define a list variable l #}
{% l.append('Charlie') %}       {# modify the list in place #}

Hello, {% l %}      {# outputs: Hello, Alice, Bob, and Charlie #}

{% foreach l %}     {# iterate over list l #}
    - {% item %}
{% /foreach %}      {# outputs: - Alice\n- Bob\n- Charlie #}
```
> ❇️ Use the walrus operator `:=` and enclosing parentheses `()` to assign a value to a variable and have it output at the same time


---
### Macros
Macros are like functions where the whole rendered body is the return value. They can take positional and keyword arguments. Macros are defined with the `{% def macro_name(...) %} ... {% /def %}` directive and called via a substitution `{% macro_name(...) %}`.
```jinja
{% def greet(name, times) %}
    {% var k = 0 %}  {# local var k; we could just use foreach's I #}
    {% foreach range(times) %}
        {% I %}: Hello, {% name %} ({% ordinal(k := k + 1) %})!
    {% /foreach %}
{% /def %}
{# Call (as expression): #}
{% greet("Alice", times=2) %}
{% greet("Bob", times=1) %}
```

Output:
```text
1: Hello, Alice (1st)!
2: Hello, Alice (2nd)!
1: Hello, Bob (1st)!
```

- Argument declaration can be omitted (no-arg macro): `{% def time_now %} ... {% /def %}`
- Macros can be recursive


---
### Imports
Import other templated text with `{% <source> %}` directive. The import source will be parsed and compiled as if it were part of the original text. Imports are always resolved during the compilation phase, so the imported content is parsed/compiled even if inside a conditional branch that doesn’t execute later. This ensures that precompilation works correctly and that all imports are available regardless of control flow during evaluation/rendering. It also ensures that subsequent evaluations will be fast since all imports are already compiled.

```jinja
{% if is_prod %}
  {% <prod_preamble.txt> %} {# must be available regardless of is_prod value #}
{% else %}
  {% <dev_preamble.txt> %}  {# must be available regardless of is_prod value #}
{% endif %}

... Other templated text here ...

{% <resource://your.pkg/templates/footer.txt> %}
```

Import sources:

- File path: `file:/path/to/file.txt` or `file:///abs/path/file.txt` or just `/path/to/file.txt`
- Package resource: `resource://package.module/path/in/package.txt` (also supports `resource:package.module/path/...`)
- Substitution: if a substitution with the exact import name exists (in `subs` arg to `evaluate`/`precompile`), its value is used as the import's source text (callables are invoked; non-strings are stringified)

Example importing from a substitution:

```python
from flextex import evaluate

partials = {
  "header.txt": "# Hi, {% name %}!\n",
}

tmpl = "{% <header.txt> %}Welcome."
print(evaluate(tmpl, {"name": "Alice", **partials}))
# -> "# Hi, Alice!\nWelcome."
```

---
### Flow control: `if` / `elif` / `else`

Conditional branching is done with the `if` / `elif` / `else` / `endif` (or `/if`) directives. The condition can be any identifier or expression that evaluates to a boolean value.

```jinja
{% if condition1 %}   {# simple substitution as condition #}
    Output if condition1 is true.
{% elif len(some_list) > 1 %}  {# expression as condition #}
    Output if condition1 is false and some_list has more than one item.
{% else %}
    Output if both condition1 and some_list are false.
{% endif %}   {# or {% /if %} #}
```
Branches can be as deeply nested as needed.

---
### Foreach
Foreach is a looping construct similar to Python's `for` loop, allowing you to iterate over a list of items and output them in a formatted way.
Inside the body of the foreach block, you have access to these special loop variables:

<small>

| Variable | Meaning        |
|---------:|----------------|
| `item`   | current item   |
| `i`      | 0‑based index  |
| `I`      | 1‑based index  |
| `count`  | total items    |
| `is_last`| True if last   |

</small>

Foreach source can be any substitution or expression that evaluates to an iterable.
- strings become single-item lists
- callables are invoked; result treated as list
- mappings with `header`/`items` are treated as nested lists. Note `body` is an alias for `items`.

Example: simple ordered list where `items = ["Apple", "Banana", "Cherry"]`:

<table><tr><th>Template</th><th>Output</th></tr><tr><td>

```jinja
{% foreach items %}
    {% I %}. {% item %}
{% /foreach %}
```

</td><td>

```text
1. Apple
2. Banana
3. Cherry
```

</td></tr></table>

Nested lists can be processed generically with `foreach` with the help of built-in utilities (or provide your own):
- check if `item` is a list: `is_list(item)` is simply `isinstance(item, list)`
- check if `item` is a nested list: `is_nested_list(item)` is simply `isinstance(item, NestedList)`
- access outer loop's index variable with `outer('I')`, `outer('item')`, etc. or just save to a new variable before the inner loop: `{% var outer_I = I %}` See [here](#outer) for more details on `outer()`.

Example: nested ordered list
```jinja
{% foreach l %}
    {% if is_nested_list(item) %}
        {% I %}. {% item.header %}
        {% foreach item.items %}
          {% outer('I') %}.{% alpha(i).lower() %} {% item %}
        {% /foreach %}
    {% else %}
        {% I %}. {% item %}
    {% endif %}
{% /foreach %}
```
If above template is evaluated with `l = [{"header": "Fruits", "items": ["Orange", "Lemon"]}, "Vegetables", "Grains"]` we get:
```text
1. Fruits
  1.a Orange
  1.b Lemon
2. Vegetables
3. Grains
```
---
### Lists

In addition to relying on the hosting application to provide your template with lists, or using the var directives, e.g., `{% var l=['Alice', 'Bob', 'Charlie'] %}`, you can define a list using the `{% list %}` directive.
Example:
```jinja
{% list items %}
  {% li %}Apples{% /li %}
  {% li %}Bananas           {# closing /li tags are optional #}
  {% li %}Cherries
{% /list %}
```
> ❇️ Closing tags `{% /li %}` are optional.
> Formatting/indentation inside the named list block is ignored/removed; only the content inside each `{% li %}` matters.

You then can use the list whenever a list is expected, e.g. in a foreach loop:
```jinja
{% foreach items %}
  {% I %}. {% item %}
{% /foreach %}
```
Of course, the real flexibility comes from being able to define lists with dynamic content using conditionals, loops, etc:
```jinja
{% list instructions %}
    {% if initial %}        {# conditional list item(s) #}
        {% li %}Start with a greeting.
        {% li %}Ask for a name.
    {% else %}
        {% li %}Welcome back, {% user_name %}.
    {% endif %}
    {% foreach steps %}     {# copy items from another iterable #}
        {% li %}{% item %}{% /li %}
    {% /foreach %}
    {% li %}Say goodbye.{% /li %}
{% /list %}
```

### Render-in-place list
By omitting the list's name, just `{% list %}`, a render-in-place (render immediately) list is created, similar to HTML's `<ul>` or `<ol>` tags, the list is rendered immediately where it is defined.
Example:

```jinja
{% list %}
    {% li %}{% I %} - London
    {% li %}{% I %} - Paris
    {% if include_us %}
        {% li %}{% I %} - New York
    {% /if %}
    {% li %}{% I %} - Tokyo
{% /list %}
```
Output:

<table><tr><th>if <code>include_us = True</code></th><th>if <code>include_us = False</code></th></tr><tr><td>

```text
1 - London
2 - Paris
3 - New York
4 - Tokyo
```
</td><td>

```text
1 - London
2 - Paris
3 - Tokyo
```
</td></tr></table>

The only reason, it seems, to use a render-in-place list block instead of just plain text is when you want to take advantage of the automatic `i`/`I` indexing variables inside the list items.

The `{% list %}` block (both render-in-place and named) has these special loop variables available:

<small>

| Variable | Meaning        |
|---------:|----------------|
| `i`      | 0‑based index of the current list item |
| `I`      | 1‑based index of the current list item |

For nested lists, either save the outer index to a new variable before the inner list, or use the convenient `outer('i')` / `outer('I')` to access the outer list's index variable from the inner list. See [`outer()`](#outer) for details.

</small>

---
### Nested lists
Since the templates can contain any arbitrary Python expressions, the objects your template receives can be anything you like. That includes nested lists - you can define them any way you like. However, when a nested list is defined like this:

```python
landmarks = [
  {'header': "France",
    'items': [{'header': "Paris", 'items': ["Eiffel Tower", "Louvre Museum", "Notre-Dame Cathedral"]},
              {'header': "Lyon",  'items': ["Basilica of Notre-Dame de Fourvière", "Parc de la Tête d'Or"]}]},
  {'header': "Italy",
    'items': [{'header': "Rome", 'items':["Colosseum", "Trevi Fountain", "Pantheon"]},
              {'header': "Venice", 'items':["St. Mark's Basilica", "Grand Canal"]}]},
]
```
It has the benefit of being easily rendered with `ul()`/`ol()` built-in functions or processed with `foreach` loops. Here, the nested lists are just mappings with `header` and `items` keys. `header` is used to define the sublist's title, and `items` is a list of either strings (list items) or further nested lists (mappings with `header` and `items`). Then inside the template, in your `foreach` loops you can access the `header` and `items` keys simply as attributes: `item.header` and `item.items`. And the built-in `is_nested_list(item)` function can be used to check if the current item is a nested list or a simple list item. In addition, the `{% list ... %}` directive defines nested lists in the same way.

Define the `landmarks` list directly in your template with `{% list %}` directive:
```jinja
{# Define the landmarks list #}
{% list landmarks %}
    {% list %} France     {# any text outside li is treated as header #}
        {% list %} Paris
            {% li %}Eiffel Tower
            {% li %}Louvre Museum
            {% li %}Notre-Dame Cathedral
        {% /list %}
        {% list %} Lyon
            {% li %}Basilica of Notre-Dame de Fourvière
            {% li %}Parc de la Tête d'Or
        {% /list %}
    {% /list %}
    {% list %} Italy
        {% list %} Rome
            {% li %}Colosseum
            {% li %}Trevi Fountain
            {% li %}Pantheon
        {% /list %}
        {% list %} Venice
            {% li %}St. Mark's Basilica
            {% li %}Grand Canal
        {% /list %}
    {% /list %}
{% /list %}
```

This nested list then can be handed to `foreach` to produce custom-formatted output:

<table><tr><th>Template</th><th>Output</th></tr><tr><td>

```jinja
{% foreach landmarks %}
    {% I %}. {% item.header %} {# Country #}
    {% foreach item.items %}   {# Cities #}
      <i>{% item.header %}</i>
      {% foreach item.items %} {# Landmarks #}
        - {% item %}
      {% /foreach %}
    {% /foreach %}
{% /foreach %}
```

</td><td>

```html
1. France
  <i>Paris</i>
    - Eiffel Tower
    - Louvre Museum
    - Notre-Dame Cathedral
  <i>Lyon</i>
    - Basilica of Notre-Dame de Fourvière
    - Parc de la Tête d'Or
2. Italy
  <i>Rome</i>
    - Colosseum
    - Trevi Fountain
    - Pantheon
  <i>Venice</i>
    - St. Mark's Basilica
    - Grand Canal
```

</td></tr></table>

---
<a id="ol_ul"></a>

### Built-in `ol()` & `ul()` list rendering functions
> 🔸 __`ol`__`(items, item_format=None, nested_indent=2, separator=None)`<br>
> 🔸 __`ul`__`(items, item_format=None, nested_indent=2, separator=None)`


Built-in `ol` and `ul` functions output ordered and unordered lists respectively.
```jinja
{% ol(items) %} {# defaults to numbered ordered list #}
===========
  {% ul(items) %} {# defaults to bulleted unordered list #}
```
If above template is evaluated with `items = ["Apple", "Banana", "Cherry"]`, the output will be:
```text
1. Apple
2. Banana
3. Cherry
===========
  - Apple
  - Banana
  - Cherry
```

Note: since we indented the `{% ul(items) %}` statement, the entire output list block is also indented accordingly in the output. This is not special to `ol`/`ul`, but rather how indentation works in FlexTex templates in general. FlexTex works hard to preserve your intended output formatting while allowing you to write readable templates.

#### Arguments:
`ol` and `ul` only differ in the default value for `item_format` argument, otherwise they behave identically.<br>
- `items`: list of items to render; can be an iterable or any expression that evaluates to an iterable
- `item_format`: format string for each item, which defaults to:
  - `ol`: `"{i}. {li}"`
  - `ul`: `"- {li}"`
  - for inline lists (rendered in a span context) defaults are:
    - `ol`: `"({i:I|.I}) {li}"` produces → `"(1) Item 1, (2) Item 2, (3) Item 3"`
    - `ul`: `"(*) {li}"` produces → `"(*) Item 1, (*) Item 2, (*) Item 3"`

  available format placeholders:
  - `{i}` : current item's index object with flexible formatting for nested indices

    <details><summary><h4 id="list-index-fspec">index format specification</h4> <i>(click to expand)</i></summary>

    Format specifier for `{i}` contains the following pattern for each nesting level; the pattern can be repeated to specify deeper levels:
    ```
    ( [~][prefix] (type | bullet ) [suffix][|] )+
    ```
    - `~`  tilde, at the beginning of the item suppresses this item's index if there are subitems present.
    - `prefix` / `suffix`: any literal string (e.g., punctuation `.`) to appear before/after the index at that level. Note these cannot contain `type` specifiers or `|`.
    - `type`:
      - `A` - capitalized alpha-index, `a` - lowercase alpha-index
      - `I` - numeric index 1-based, `i` - numeric index 0-based; zero-pad with `0` (e.g., `0I` or `00i`)
      - `R`/`r` - Roman numeral upper/lower
    - `bullet`: any sequence in backticks `..` represents a 'bullet' used instead of a `type`, e.g., '`*`' or '`-`'. Empty backticks suppress the index at that level.
    - `|` ends the item format, e.g., `"i:I|.A"` results in no trailing period, as opposed to `"i:I.A."` which always has a trailing period
    - Repeat the last spec for deeper levels automatically

    Examples:

    <small>

    | Specifier | Output |
    |:------:|---------|
    | `{i}` | Defaults to `{i:I.}` → `1.` `1.2.`  |
    | `{i:I.}` | Numeric 1-based with trailing dot each level → `1.` `1.2.` |
    | `{i:I\|.A}` | Level 1 → `1` &nbsp; `2` _(no trailing period)_<br>Level 2<sup>+</sup> → `1.A` &nbsp; `1.B.A` |
    | `{i:A.a.I}` | Level 1 → `A.` &nbsp; `B.`<br>Level 2 → `A.a.` &nbsp; `A.b.`<br>Level 3<sup>+</sup> → `A.a.1` &nbsp; `A.a.2` |
    | `{i:(A)/(a)/}` | Wrap in `( )` with `/` as separators<br>→ `(A)/(b)/` &nbsp; `(A)/(a)/(b)` |
    | `{i:~A.a}` | Suppress 1st level output if deeper nesting present<br>Level 1 → `A.`<br>Level 2 → `a` |
    | ``{i:`*`}`` | Use literal bullet (any sequence really) → `*` &nbsp; `**` &nbsp; `***` |
    | `{i:0I.00I}` | Zero pad → `01.` &nbsp; `01.002` |
    | `{i:R.r}` | Upper / lower Roman numerals → `I.` &nbsp; `II.iv` |

    </small>
    </details>

  - `{I}` : 1-based index of the current item
  - `{li}` : list item content.
- `nested_indent` _(int or string)_: number of spaces (or any filling string) to use for nested lists (default is 2). The nested lists are handled automatically based on the type of items in the list (if an item is itself a list or `NestedList`, it is treated as a nested list).
- `separator` _(string or callable)_: string to use to separate items (default is `", "` for spans, `"\n"` for block elements); or a callable that joins preformatted item strings.

#### Formatting nested lists
Consider this example rendering twice the nested list of `landmarks` defined previously:
```python
default = evaluate("\n{% ol(landmarks) %}", {'landmarks': landmarks})
custom  = evaluate("\n{% ol(landmarks, item_format='{i:~I. |~`<i>`|`- `}{li}{i:``|~`</i>`|``}') %}",
                   {'landmarks': landmarks})
```
Output:

<table><tr><th>default</th><th>custom</th></tr><tr><td>

```text
1. France
  1.1. Paris
    1.1.1. Eiffel Tower
    1.1.2. Louvre Museum
    1.1.3. Notre-Dame Cathedral
  1.2. Lyon
    1.2.1. Basilica of Notre-Dame de Fourvière
    1.2.2. Parc de la Tête d'Or
2. Italy
  2.1. Rome
    2.1.1. Colosseum
    2.1.2. Trevi Fountain
    2.1.3. Pantheon
  2.2. Venice
    2.2.1. St. Mark's Basilica
    2.2.2. Grand Canal
```

</td><td>

```html
1. France
  <i>Paris</i>
    - Eiffel Tower
    - Louvre Museum
    - Notre-Dame Cathedral
  <i>Lyon</i>
    - Basilica of Notre-Dame de Fourvière
    - Parc de la Tête d'Or
2. Italy
  <i>Rome</i>
    - Colosseum
    - Trevi Fountain
    - Pantheon
  <i>Venice</i>
    - St. Mark's Basilica
    - Grand Canal
```

</td></tr></table>

<a id="en_and_or"></a>

### Inline lists (natural language)

Inline (span) list serialization is automatic in some contexts and also available via helpers:
- `en_and(items)` → "a, b and c"
- `en_comma_and(items)` → "a, b, and c"
- `en_or(items)` → "a, b or c"
- `en_comma_or(items)` → "a, b, or c"

You can also render inline lists with `{% ol(items) %}` or `foreach` in inline (span) contexts:
```jinja
{# inline list of items, using span foreach #}
{% foreach items %}{% item %}{% if not is_last %}, {% endif %}{% /foreach %}
Outputs: a, b, c
```

---
### Template metadata - `meta`
Template-level metadata can be specified using the `{% meta key=value %}` directive. This allows you to set options that affect the entire template. For example, you can set the structural indentation level for the template using:
```jinja
{% meta indent=2 %}
```
This sets the structural indentation to 2 spaces instead of the default 4.

- Meta settings apply to the template text following the directive.
- Meta is processed at compile time and should be used at the top level (this is enforced).
- Meta key without a value (e.g., `{% meta indent %}`) resets to the default value.
- Meta does not cross import boundaries; each imported template can have its own meta settings.
- Supported metadata keys:
  - `indent` is the only supported key currently; value must be a non-negative integer specifying the number of spaces for structural indentation; default is 4.

## Built-in functions (available in expressions)

The following functions are injected automatically into the expression scope and can be used to format numbers and lists:

<small>

| Function | Description |
|---------|----------------|
| `alpha(n)` | Spreadsheet-style letters (`A`, `B`, …, `Z`, `AA`, `AB`, …)  |
| `ordinal(n)` | English ordinal string (`1st`, `2nd`, …)  |
| `roman(n)` | Roman numerals (`I`, `II`, … `XII`, …)<br>Use `roman(n).lower()` for lowercase. |
| `cardinal(n, sep=' ')` | English cardinal words (`twenty one`)<br>Use `sep='_'` for snake form (`twenty_one`). |
| [`en_and(items)`](#en_and_or)<br>[`en_comma_and(items)`](#en_and_or) | Inline list joined with commas + `and`<br>Second form uses Oxford comma. |
| [`en_or(items)`](#en_and_or)<br>[`en_comma_or(items)`](#en_and_or) | Inline list joined with commas + `or`<br>Second form uses Oxford comma. |
| [`ol(items,item_format=None,`<br>`   nested_indent=2)`](#ol_ul) | Ordered list renderer. |
| [`ul(items, item_format=None,`<br>`   nested_indent=2)`](#ol_ul) | Unordered list renderer. |
| `is_list(v)` | Type check for Python `list`. Strictly checks for `list` (not any sequence). |
| `is_nested_list(v)` | Check for FlexTex nested list. True for `NestedList` items. |
| [`outer(var_name, level=1)`](#outer) | Access outer-scope variable. Only needed when shadowed |

</small>

Examples:
```jinja
{% alpha(28) %}            {# -> "AB" #}
{% alpha(28).lower() %}    {# -> "ab" #}
{% ordinal(23) %}          {# -> "23rd" #}
{% roman(1999) %}          {# -> "MCMXCIX" #}
{% roman(1999).lower() %}  {# -> "mcmxcix" #}
{% cardinal(42, '-') %}    {# -> "forty-two" #}
{% cardinal(42).title() %} {# -> "Forty Two" #}
{% is_list([a,2,3]) %}     {# -> True #}
{% en_and(["apples", "bananas", "cherries"]) %}  {# -> "apples, bananas and cherries" #}
{% en_comma_and(["apples", "bananas", "cherries"]) %}  {# -> "apples, bananas, and cherries" #}
```
---
<a id="outer"></a>

#### 🔸 __`outer`__`(var_name, level=1)`

Access a variable from an enclosing scope when an inner block shadows it (common in nested `foreach`/`list` loops for automatic loop variables like `item`, `i`, `I`).

- Args:
  - `var_name` (str): Variable name to fetch (e.g., `"I"`).
  - `level` (int, default 1): Scopes to climb; 1 = immediate outer.

- Returns: The value of the outer variable.

- Notes:
  - Not needed unless a same‑named inner variable hides the outer one.
  - Alternative: save the outer var to a new name before the inner block.

Example (both approaches produce the same output):

<table><tr><th>using differently named var</th><th>using <code>outer()</code></th></tr><tr><td>

```jinja
{% foreach categories %}
    {% I %}. {% item.header %}
    {% var cI = I %}  {# save outer I #}
    {% foreach item.items %}
      {% cI %}.{% I %} - {% item %}
    {% /foreach %}
{% /foreach %}
```

</td><td>

```jinja
{% foreach categories %}
    {% I %}. {% item.header %}
    {% foreach item.items %}
      {% outer('I') %}.{% I %} - {% item %}
    {% /foreach %}
{% /foreach %}
```

</td></tr><tr><td colspan="2">

```text
1. Fruits
  1.1 - Apple
  1.2 - Banana
2. Vegetables
  2.1 - Carrot
  2.2 - Lettuce
```

</td></tr></table>


## Python API Reference
### 🔸 `evaluate(tmpl | ast, subs=None) -> str`
Compile (if `tmpl` is text) or reuse a precompiled AST, then evaluate and render to text.

- Args:
  - `tmpl` <small>_(str | AST)_</small>: Template text or a precompiled `AST`. You can directly process a file or resource by passing `tmpl` containing a single import directive:
    ```python
    txt = evaluate("{% <path/to/file.txt> %}", subs)   # file OR resource
    txt = evaluate("{% <resource://module.sub-module/resource_name> %}", subs)
    ```
  - `subs` <small>_(dict | tuple\[dict, ...], optional)_</small>: Substitutions available at evaluation time. If a tuple, earlier dicts take precedence. Substitutions may contain simple strings, callables, or any other objects. If you use expressions in the template, the `subs` must contain the necessary context for those expressions to evaluate correctly.
- Returns:
  - `str` : Rendered template text.
- Exceptions:
  - Raises `FlextexError` with detailed source context on compile/render failures.

Examples:
```python
from flextex import evaluate

# render string template
txt = evaluate("Hi {% name %}", {"name": "Alice"})

# evaluate a file via import:
txt = evaluate("{% </abs/path/template.txt> %}", {"name": "Alice"})

# package resource:
txt = evaluate("{% <resource://mypkg.templates/welcome.txt> %}", {"name": "Ada"})
```

Substitutions vs. expressions:
```python
from datetime import datetime
from flextex import evaluate

def now_iso(): # return current time in ISO 8601 format
    return datetime.now().isoformat(timespec="seconds")

# simple substitution: does NOT use eval(), flextex auto-invokes callables
tmpl = "Generated at {% now_iso %}"
print(evaluate(tmpl, {"now_iso": now_iso}))
# -> Generated at 2004-09-04T12:34:56

# Python expression: uses eval() to call now_iso()
tmpl = "Generated at {% now_iso() %}"
print(evaluate(tmpl, {"now_iso": now_iso}))
# -> Generated at 2004-09-04T12:34:56

# Python expression with more complex logic:
tmpl = "Generated at {% datetime.now().isoformat(timespec='seconds') %}"
print(evaluate(tmpl, {"datetime": datetime}))
# -> Generated at 2004-09-04T12:34:56

```
---
### 🔸 `precompile(tmpl, subs=None, noeval=False) -> AST`
Compile the template into an AST with imports resolved. Use the result with `evaluate` for fast repeated rendering.

- Args:
  - `tmpl` <small>_(str | Source | tuple\[name, data])_</small>: Template text, a `Source`, or `(name, data)`. You may also pass a single import directive to compile a file/resource:
    ```python
    ast = precompile("{% </abs/path/file.txt> %}", subs)
    ast = precompile("{% <resource://pkg.mod/template.txt> %}", subs)
    ```
  - `subs` <small>_(dict | tuple\[dict, ...], optional)_</small>: Substitutions used by the template's import `{% <inc_this> %}` directives. If a tuple, earlier dicts take precedence.  Substitutions should contain strings, callables, or any other objects convertible to import's source text.
  - `noeval` <small>_(bool, default False)_</small>: Disable expression evaluation for this template. Global `NOEVAL` (see `set_noeval`) takes precedence.
- Returns:
  - Compiled template (`AST`). Use with `evaluate` to render.
- Exceptions:
  - Raises `FlextexError` with detailed source context on failures.
- Notes:
  - Imports are always resolved during precompile (even inside branches), so later evaluations are fast.


Typical reuse pattern (precompile once, render many):
```python
from flextex import precompile, evaluate

tmpl = precompile("{% <templates/welcome.txt> %}")
for name in ["Sneezy", "Sleepy", "Dopey", "Grumpy"]:
    print(evaluate(tmpl, {"name": name}))
```
---

<a id="set_indent"></a>

### 🔸 `set_indent(indent: int|None) -> int`
Sets the global indentation level for all templates. Returns previous value.
- Args:
  - indent (int|None):
    - If None, leaves INDENT_SIZE unchanged; returns current value.
    - If int, sets INDENT_SIZE to given value (must be >=0).

> ➰ _Notes:_
>  - Default global indent is 4 spaces; check current value with: `print(set_indent(None))`
>  - Template [`{% meta indent=<value> %}`](#directives) directive sets template-level indentation. Template's meta settings take precedence over the global setting.

Example:
```python
from flextex import set_indent, evaluate

tmpl = """
{% if True %}
  hello
{% endif %}"""

print(evaluate(tmpl)) # ->  "  hello"  (structural indent is 4 spaces by default)

set_indent(2)  # set global structural indent to 2 spaces

print(evaluate(tmpl)) # ->  "hello"  (structural indent is 2 spaces now)
```
---
### 🔸 `set_noeval(noeval: bool|None) -> bool`
Disables evaluation of expressions globally (security/safe-mode). Returns previous value.
- Args:
  - noeval (bool|None):
    - If None, leaves NOEVAL unchanged; returns current value.
    - If `True` disables eval() in all templates.
    - If `False` enables eval() globally; you can still disable per-template eval(). (default)

> ➰ <b>_Notes:_</b>
> - See `precompile(..., noeval=True)` to disable eval() on per-template basis. The global setting always takes precedence.

Example:
```python
from flextex import set_noeval, evaluate

set_noeval(True)  # disable eval for all templates

# Any attempt to compile an expression will fail with FlextexError
# with exact source location of the offending expression.
text = evaluate("Value: {% 1 + 2 %}")
# ->  FlextexError: Failed to compile `substitution`: Evaluation is disabled
# ->  <template>:1:11: `Value: {% 1̲ + 2 %}`
```

## Error Reporting

Whenever there is an error, `FlextexError` is raised providing context-rich information to help locate and fix the problem. Error reports include:

- Precise line/column with underlined character
- Import chain trace (shows originating import site)
- Differentiates compile vs evaluation vs conversion vs render failures
- Provides specific messages (e.g., invalid macro arg, unclosed block, unknown variable)

Example:

```
FlextexError: Reference 'foo' not found
<foo.txt>:12:7: `  {% foo̲ %}`
Imported from <common.ftx>:3:5: `  {% <foo.txt> %}`
```

## CLI Reference
Flextex module can be used as a command-line tool to render/compile simple templates. The substitutions can be provided as a JSON object via standard input.

```sh
python -m flextex --version         # show version
python -m flextex -e "Hello {% name %}" <<< '{"name":"World"}' # evaluate inline template
python -m flextex -e example.flex   # if existing file, treat as "{% <example.flex> %}" otherwise as template text
python -m flextex -c "Hello {% name "  # compile inline template, see if there are errors
```

## Performance Notes
- Evaluating a template is a two-step process: compile the template into AST (`precompile`) and then evaluate or render the resulting AST. Precompile once, render many: parsing, AST building, and import resolution happen during `precompile`, so reusing the returned `AST` avoids a lot of repeated work.
- Expression bytecode cache: non-identifier expressions are compiled once (on first evaluation) and cached on the AST/node, so repeated uses (e.g., inside `foreach`) are fast. This applies to:
  - Direct `evaluate` calls when the same expression appears multiple times.
  - All subsequent `evaluate(ast, ...)` calls for a precompiled AST (cache is reused across runs).

## Thread Safety
- FlexTex keeps no global state and is safe to use from multiple threads concurrently. In particular: It is safe to precompile a template and use the same compiled template from multiple threads.
- Global flag caveat: `set_noeval(True/False)` changes a process‑wide flag. Avoid flipping it while other threads are evaluating. Prefer per‑AST `noeval=True`.

## Security Notes
- Do not evaluate untrusted templates or untrusted substitutions.
- If you use Python expressions and statements and not simple substitution identifiers inside `{% ... %}` directives these are evaluated using Python’s `eval`/`compile` in a restricted environment that exposes only the substitutions you provide and the built-ins listed above. Only when the substitutions are not identifiers `eval`/`compile` is used; simple identifier substitutions are looked up directly.
- Disable evaluation when needed:
  - Per individual template: `precompile(..., noeval=True)` then `evaluate(ast, ...)` as usual.
  - Globally: `set_noeval(True)` before any precompilation/evaluation.
- In `noeval` mode, only simple identifier substitutions are allowed (substitutions can still be callables that you define and provide so it's not all doom and gloom); any other expression or statement will result in an error. Of course, this means your hosting application will have to provide all the necessary substitutions directly. Note that you still have full flow control with `if`, `foreach`, lists, macros, imports, etc.; only Python expression evaluation is disabled.

## Development & Contributing
See [DEV.md](DEV.md) for guidelines. See also [CHANGELOG.md](CHANGELOG.md).

## License

MIT (see LICENSE).

[↑ Back to top](#table-of-contents)

