Awee

How It Works

The dependency graph, resolver, and modifier pipeline explained.

Sintax is built around three ideas: parse expressions into tokens, build a dependency graph across all variables, and apply modifiers as a pipeline.

Parsing

Every string value is scanned for {{ }} expressions. Each expression is tokenised into one of:

TokenExample
Plain variable{{ name }}
Filtered variable{{ text | trim | upper }}
If block{{ if condition }}...{{ else }}...{{ endif }}
For block{{ for v in items }}...{{ endfor }}
For with index/key{{ for i, v in items }}...{{ endfor }}
Ternary{{ x | gt:0 ? 'yes' : 'no' }}
Whitespace trim{{- expr -}}

The parser extracts variable references and modifier chains but does not evaluate anything yet. This separation lets the resolver build the dependency graph before any rendering happens.

The problem it solves

Most template engines assume you hand them a fully-resolved context. Sintax assumes your context is the thing that needs resolving.

# Input - variables that reference each other
env:      "production"
prefix:   "{{ env | upper }}"
db_name:  "{{ prefix }}_database"

# Output - resolved in dependency order
env:      "production"
prefix:   "PRODUCTION"
db_name:  "PRODUCTION_database"

Dependency resolution

Parse - scan every variable value, collect which other variables it references

Build graph - construct a directed acyclic graph (DAG) where edges point from dependency to dependent

Sort - topologically sort the graph to get a safe resolution order

Resolve - process variables one by one, memoising each result so nothing is evaluated twice

Detect cycles - if the graph contains a cycle, return a typed error before any rendering occurs

Because variables are resolved in dependency order, by the time {{ prefix }}_{{ suffix }} is evaluated, both prefix and suffix are already final values.

Modifier pipeline

Each {{ value | mod1 | mod2:arg }} expression runs the value through a left-to-right pipeline:

initial value
 mod1(value)             result1
 mod2(result1, [arg])    result2
 output

Modifier arguments can be:

TypeSyntax
String literal'quoted' or "quoted"
Number42, 3.14
Booleantrue, false, yes, no
Variable referenceunquoted name resolved from context

The default modifier is special - it catches missing-variable and modifier errors and substitutes its argument instead, letting you build resilient pipelines without extra branching.

Nested data

Sintax resolves interpolations recursively through any nested structure:

TypeBehaviour
Mapskeys and values are both resolved
Slicesevery element is resolved
Structsexported fields are resolved
Pointersdereferenced, then resolved

This means a slice of maps each containing template strings resolves correctly with no extra configuration.

Conditional logic

{{ if is_admin }}Admin panel{{ else }}Dashboard{{ endif }}

{{ count | gt:0 ? 'items found' : 'nothing here' }}

Conditions use any expression that resolves to a truthy value. The gt, gte, eq, is, and not modifiers are built for this. Block tags are closed with endif (and endfor for loops).

Loops

{{ for v in xs }} ... {{ endfor }} iterates over slices, arrays, and maps. Two binding forms are supported:

{{ for v in xs }} ... {{ endfor }}        # bind value
{{ for i, v in xs }} ... {{ endfor }}     # bind index/key + value

Inside the body these helpers are auto-bound (where <v> is the value name you chose):

BindingSet on
<v>_indexslice and map iterations (0-based)
<v>_first, <v>_lastbooleans for the first/last iteration
<v>_keymap iterations only, when no explicit key name was bound

Map iteration is sorted by key. Loops nest freely and outer-scope variables remain visible in the body.

Whitespace control

A leading or trailing - inside a tag eats whitespace on that side, the same way Jinja and Go templates do:

PatternEffect
{{- expr }}strip trailing whitespace (including newlines) from the text before this tag
{{ expr -}}strip leading whitespace (including newlines) from the text after this tag
{{- expr -}}both at once

Block control tags (if/else/endif/for/endfor) that sit alone on their own line are auto-trimmed — the surrounding indentation and the line's newline are removed automatically, so block-heavy templates stay readable without - on every tag.

Safe rendering

RenderSafe skips re-interpolating variable values. Use it when rendering a template that was already produced by a previous resolution step - it prevents double-interpolation and keeps output stable.

Error model

All errors are typed sentinels matchable with errors.Is:

ErrorWhen
ErrVariableNotFoundA referenced variable is not in the context
ErrCircularDependencyVariable A → B → A detected in the graph
ErrFunctionNotFoundAn unknown modifier name was used
ErrFunctionApplyFailedA modifier returned an error
ErrParseFailedThe template expression could not be parsed
ErrRenderFailedExecution failed during rendering

Special variables

A handful of names are resolved dynamically before the dependency graph runs:

NameProduces
{{ uuid }}UUID v4 string
{{ uuidv1 }}UUID v1 string
{{ now }}current date-time - pipe into format
{{ [] }}empty array - useful with default
{{ {} }}empty object - useful with default

Extending the modifier set

Modifiers are plain functions with a single signature. Pass overrides when creating the engine to add or replace any modifier - custom ones participate in the same pipeline and error model as built-ins.

How is this guide?

On this page