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 }}...{{ /if }}
Ternary{{ x | gt:0 ? 'yes' : 'no' }}

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{{ /if }}

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

Conditions use any expression that resolves to a truthy value. The gt, gte, eq, and not modifiers are built for this.

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