Overview
An overview of the functionality and the modifier functions.
A zero-dependency templating engine for Go, built for workflows, document generation, and data transformations.
{{ response | from_json | key:'orders' | filter:'status','paid' | pluck:'total' | sum | decimal:2 }}That one line takes a raw JSON response, drills into the orders, keeps the paid ones, pulls out every total, adds them up, and formats the result to two decimals. No Go code, no helper functions, no glue.
Getting started
go get github.com/toaweme/sintaximport (
"github.com/toaweme/sintax"
"github.com/toaweme/sintax/defaults"
)
out, err := sintax.Render(
"{{ users | filter:'active',true | pluck:'email' }}",
vars,
defaults.All(),
)Here out is the value the template produced, not text. Render hands back any, so a pipeline that ends in a slice of emails gives you that slice with its type intact. When text is what you want, call RenderString instead and sintax formats the result for you.
The full source lives on GitHub. For the Go API in depth, see the Quick Guide.
The problem
Go's text/template was built to render HTML and text. The moment you need to actually transform data, filter a slice, sum a column, parse embedded JSON, you leave the template and write Go.
Sintax moves the transformation into the template, where you can read it top to bottom.
Why sintax
- Familiar syntax. Jinja2-style pipes that read left to right. If you have written a Django or Ansible template, you already know it.
- 74 built-in modifiers. String operations, collection operations, conversion, formatting, file paths, and defaults, all shipping in the box.
text/templateships around 14. - Returns values, not just text.
Renderreturnsany. A template that resolves to a slice gives you a slice, with its type intact, not a stringified version of one. Reach forRenderStringwhen the output is a document and you want text. - Typed errors. Parse failures, missing variables, and circular references surface as distinct sentinel errors you can branch on with
errors.Is. - Dependency resolution. Variables can reference each other, and sintax resolves them in the correct order. Circular references are caught at resolution time, not at runtime.
- Zero dependencies. The core imports only the standard library. Nothing to audit, nothing to keep offline, nothing that ships a transitive tree behind your back.
- Extensible. Register your own modifiers as a single function type, or override a built-in by name.
What it looks like
Chain modifiers to transform any value in one expression.
{{ text | trim | upper }}
{{ items | join:',' }}
{{ value | default:'n/a' }}Loop over slices and maps with auto-bound index, first, last, and key helpers.
{{ for i, u in users }}{{ i }}: {{ u | key:'email' | lower }}
{{ endfor }}Branch with conditionals.
{{ if active }}yes{{ else }}no{{ endif }}The Template Syntax page covers all of these in full, along with whitespace control.
Include and render another template, with a recursion guard and a directory whitelist for safety.
{{ 'partial.tpl' | file | template }}Where it fits
Reach for sintax when you are generating documents, shaping data for an API call, or wiring template-driven steps into a workflow, and you want the transformation to live in the template instead of scattered across Go helpers.
Reach for html/template instead when you are rendering HTML from untrusted input.
Sintax ships manual escape modifiers (escape_html, escape_js, escape_url), but it does not auto-escape by context the way the standard library does, and that automatic, context-aware escaping is what keeps untrusted input safe.
Modifiers
Each modifier function links to its own page with full inputs, parameters, and examples.
Casing
Shift case: upper, lower, title, and slug.
lowerConverts a string to lowercase.{{ name | lower }}slugConverts a string to a URL-friendly slug.{{ title | slug }}titleConverts a hyphen-separated slug into a title-cased string.{{ slug | title }}title_modelFormats an AI model identifier into a human-readable title.{{ model | title_model }}upperConverts a string to uppercase.{{ code | upper }}Edit
Replace, wrap, shorten, and reshape strings.
concatJoins the value and every part into one string, in order, with no
separator.{{ name | concat:'-','01','.txt' }}hashReturns the lowercase hex digest of the value under the named algorithm,
one of sha256, sha512, sha1, or md5.{{ token | hash:'sha256' }}replaceReplaces every occurrence of the old substring with the replacement in
the input.{{ text | replace:'world','everyone' }}replace_patternReplaces every match of the RE2 pattern in the input with the
replacement, which may reference capture groups with $1, $2 and so on.{{ name | replace_pattern:'(\w+), (\w+)','$2 $1' }}reverseReverses the input by rune, so multi-byte characters stay intact.{{ word | reverse }}shortenTruncates the input to at most a given number of bytes, returning it
unchanged when it is already shorter.{{ text | shorten:5 }}wrapNests the value inside a new single-entry map under the given key.{{ value | wrap:'name' }}Split & join
Split strings into parts and join them back together.
Trim
Strip whitespace, prefixes, and suffixes.
trimStrips any of the cutset characters from both ends of a string.{{ input | trim:'/' }}trim_prefixRemoves the given prefix once from the start of a string,
returning it unchanged when it does not start with that prefix.{{ path | trim_prefix:'/api' }}trim_suffixRemoves the given suffix once from the end of a string,
returning it unchanged when it does not end with that suffix.{{ file | trim_suffix:'.txt' }}Escape
Escape strings for HTML, URLs, and JavaScript.
Format
Format numbers and strings for display.
currencyConverts a numeric value between currency units by scaling it with a
ratio of unit sizes: the result is value * toUnits / fromUnits, truncated to a
whole integer.{{ price | currency:1,100 }}dateRenders a date/time value using a PHP-style date layout (Y is the
4-digit year, m the zero-padded month, d the day, H:i:s the time, and so on).{{ at | date:'Y-m-d' }}decimalFormats a number with the given number of decimal places,
rounding to the nearest value at that precision.{{ ratio | decimal:4 }}from_dateReads a date out of a string using the same PHP-style layout the
date modifier writes with, so June 26, 2026 piped through
from_date:'F j, Y' becomes a real time.Time that a date column can take.{{ printed | from_date:'F j, Y' | date:'Y-m-d' }}lengthReturns the number of UTF-8 bytes in a string, so a multi-byte
character such as "é" counts as more than one.{{ name | length }}line_numbersPrepends each line of the input with a line number counting up
from a given start line, so line_numbers:6 renders a block as if it began at
line six.{{ body | line_numbers:6 }}Access
Reach into arrays and maps by key or position.
findReturns the first map in a slice whose named field equals the wanted
value, scanning in order and returning the whole matching map.{{ items | find:'status','active' }}firstReturns the leading byte of a string as a one-byte string, so it
is a letter only for ASCII text; a multi-byte character yields a broken
fragment.{{ word | first }}keyReads one value out of a map by key or out of a slice by index.{{ config | key:'database.host' }}lastReturns the trailing byte of a string as a one-byte string, with
the same ASCII caveat as the first modifier.{{ word | last }}pluckReads one named field from every element of a slice of maps and returns
the collected values as a slice, in order.{{ users | pluck:'name' }}Query
Filter, test, and search collections.
filterReturns the items of a slice whose named field equals the search value.{{ items | filter:'role','admin' }}hasReports whether a collection contains something, and what "contains"
means depends on the shape of the value.{{ tags | has:'featured' }}isReports whether the value equals any one of the given candidates, a compact
way to write an "is this one of these" test in a template.{{ status | is:'active','pending' }}Transform
Sort, map, merge, and fold collections.
cleanDrops the elements that hold nothing, so a list can be built by
appending unconditionally and tidied once at the end instead of being guarded
at every append.{{ parts | clean }}flattenFlattens a slice by exactly one level: any element that is itself a
slice or array is spread into the result, and every other element is copied
through unchanged.{{ groups | flatten }}mapConverts a slice of string-keyed maps into a single map keyed by the named
field's value, turning a list you have to scan into a lookup table.{{ users | map:'id' }}mergeKeys a slice of maps by the named field, producing a lookup map.{{ users | merge:'id' }}sortSorts a copy of a slice in the named direction, 'asc' or 'desc'; any
other direction is an error.{{ scores | sort:'desc' }}sumTotals the named field across a slice of maps, the way you sum one
column of a list of records.{{ items | sum:'price' }}File
Read a file's contents into a template.
Path
Pull apart file paths - directory, name, and extension.
dirnameReturns the directory portion of a file path, everything up to but not
including the final path element.{{ path | dirname }}extReturns the file extension without the leading dot, for example
"png" for "avatar.png".{{ path | ext }}ext_dotReturns the file extension including the leading dot, for
example ".png" for "avatar.png".{{ path | ext_dot }}ext_prependInserts an extra extension segment just before the existing
file extension, so "styles.css" with "min" becomes "styles.min.css".{{ path | ext_prepend:'min' }}ext_trimReturns the file path without its trailing extension.{{ path | ext_trim }}filenameReturns the base file name from a path, the final path element
including its extension.{{ path | filename }}Parse
Read JSON, YAML, and other formats into values.
from_csvParses a CSV string into a list of rows, so a serialized table (a
report export, a spreadsheet dump) becomes data that later template steps can
filter and pluck.{{ body | from_csv }}from_jsonParses a JSON object string into a map, so a serialized payload (an
API response body, a config blob) becomes data that later template steps can
index into.{{ body | from_json }}from_yamlParses a YAML document into a map, so a config file or a manifest
becomes data that later template steps can index into.{{ body | from_yaml }}Serialize
Render values as JSON, YAML, and Markdown.
jsonSerializes a value to a compact JSON string.{{ user | json }}markdownConverts an HTML string to Markdown, so a fetched page or a rich-text
field can be reduced to plain prose a later step can store or render.{{ body | markdown }}yamlSerializes a value as YAML, so a map assembled in a template can be
written out as a config file or a manifest.{{ config | yaml }}Compare
Compare values for use in conditional expressions.
andReports whether the piped value and its argument are both truthy, reading
truthiness the way a template if does.{{ enabled | and:has_rows }}eqReports numeric equality across the int and float kinds, so 5 equals
5.0.{{ total | eq:5.0 }}gtReports whether the value falls after the given date, the gt clause
reached when both sides are real dates, so {{ expires_at | gt:now }} asks
whether a date is still ahead.{{ expires_at | gt:today }}gteReports whether the value falls on or after the given date, the same
stance GtTime takes on what counts as a date.{{ score | gte:70 }}ltReports whether the value falls before the given date, so
{{ starts_at | lt:now }} asks whether a date has passed.{{ starts_at | lt:today }}lteReports whether the value falls on or before the given date, the
same stance LtTime takes on what counts as a date.{{ days_left | lte:14 }}neqReports numeric inequality across the int and float kinds, so 5 and
5.0 are equal and this returns false for them.{{ count | neq:5.0 }}notInverts the truthiness of the value, following the same rules as a
template if.{{ name | not }}orReports whether either the piped value or its argument is truthy, reading
truthiness the way a template if does.{{ draft | not | or:published }}Default
Supply a fallback when a value is missing.
Template
Render a nested template.
Datetime
add_daysShifts a date by a whole number of days, forward on a positive
amount and back on a negative one, so {{ now | add_days:14 | date:'Y-m-d' }}
is the date a fortnight from today and add_days:-7 is the date a week ago.{{ today | add_days:14 | date:'Y-m-d' }}add_monthsShifts a date by a whole number of calendar months, forward on a
positive amount and back on a negative one.{{ charged_on | add_months:1 | date:'Y-m-d' }}add_yearsShifts a date by a whole number of years, forward on a positive
amount and back on a negative one.{{ bought_on | add_years:years | date:'Y-m-d' }}days_betweenCounts the whole days from the value to the argument, positive
when the argument is the later of the two and negative when it is earlier, so
{{ now | days_between:expires_at }} is how many days are left.expires in {{ today | days_between:expires_at }} daysMath
addReturns the value plus the given number, so {{ subtotal | add:shipping }}
is the two of them together.{{ subtotal | add:shipping | decimal:2 }}divideReturns the value split by the given number, which is an average from
a total and a count, or a share from an amount and a number of people.{{ total | divide:orders | decimal:2 }}multiplyReturns the value scaled by the given number, which is a line total
from a price and a quantity, a tax-inclusive amount from a rate, or a count
converted into a period.{{ net | multiply:1.21 | decimal:2 }}subtractReturns the value less the given number, the way a remaining
quantity is worked out from a total and what has gone, as in
{{ seats | subtract:booked }}.{{ seats | subtract:booked }} left