template

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 18 Imported by: 0

README

Template Engine

A lightweight Go template engine with Django-inspired control flow and Liquid-compatible filter syntax. Supports variable interpolation, 80+ built-in filters, conditionals, loops, and — when you opt in — multi-file templates with inheritance, includes, raw blocks, and HTML auto-escape.

This engine implements the Liquid filter standard with 41 out of 46 filters fully compliant, plus 20+ extension filters. See Liquid Compatibility for details.

Two Modes, One Package

Like Go's own text/template vs html/template, this library exposes two complementary APIs:

Mode API Use for
Single-string template.Compile(src) / template.Render(src, ctx) Log lines, nested config snippets, quick interpolation. Zero dependencies on loaders.
Multi-file text template.NewTextSet(loader) Code generation, YAML/TOML/Taskfile, plain-text emails. Supports include / extends / block / raw, no HTML escape.
Multi-file HTML template.NewHTMLSet(loader) Web pages, HTML emails, PDF reports. Everything NewTextSet does, plus automatic HTML escape with SafeString / {{ x | safe }}.

Guiding principle: if you only use Compile(src), the engine behaves exactly as before the layout features landed — no new tags, no new filters, no surprises. Layout features live in Set-scoped registries and never leak into the global path.

Installation

go get github.com/kaptinlin/template

Quick Start

Single-string rendering

For log lines, config snippets, and anything where you just want to interpolate a few values into a string:

output, err := template.Render("Hello, {{ name|upcase }}!", template.Context{
    "name": "alice",
})
// output: "Hello, ALICE!"
Compile and reuse

For templates rendered multiple times:

tmpl, err := template.Compile("Hello, {{ name }}!")
if err != nil {
    log.Fatal(err)
}

output, err := tmpl.Render(template.Context{"name": "World"})
// output: "Hello, World!"
Multi-file HTML with layout inheritance

For web pages with shared layouts, includes, and auto-escape:

import (
    "embed"
    "os"

    "github.com/kaptinlin/template"
)

//go:embed themes/default/*
var themeFS embed.FS

func main() {
    user, _ := template.NewDirLoader("./templates")               // os.Root sandbox
    theme  := template.NewFSLoader(themeFS)                        // embed.FS
    loader := template.NewChainLoader(user, theme)                 // user > theme

    set := template.NewHTMLSet(loader,
        template.WithGlobals(template.Context{"site": siteData}),
    )

    _ = set.Render("layouts/blog.html", template.Context{
        "page": map[string]any{
            "title":   "Hello <world>",                            // auto-escaped
            "content": template.SafeString("<p>trusted HTML</p>"), // rendered as-is
        },
    }, os.Stdout)
}

The templates in ./themes/default/:

{# layouts/base.html #}
<!DOCTYPE html>
<html>
<head>{% block head %}<title>{{ site.title }}</title>{% endblock %}</head>
<body>
  {% include "partials/header.html" %}
  <main>{% block content %}{% endblock %}</main>
</body>
</html>
{# layouts/blog.html #}
{% extends "layouts/base.html" %}

{% block head %}
  {{ block.super }}
  <meta name="description" content="{{ page.title }}">
{% endblock %}

{% block content %}
  <article>
    <h1>{{ page.title }}</h1>
    {{ page.content | safe }}
  </article>
{% endblock content %}

See docs/layout.md for the full layout guide, docs/loaders.md for loader configuration, and docs/security.md for the security model.

Multi-file text generation

For generating code, config files, or anything that should NOT be HTML-escaped:

loader, _ := template.NewDirLoader("./scaffold")

set := template.NewTextSet(loader,
    template.WithGlobals(template.Context{"project": projectMeta}),
)

// Generates Taskfile.yml with {{.GOBIN}}/bin intact — no HTML escape would
// turn & into &amp; and break the YAML.
_ = set.Render("Taskfile.yml.tmpl", nil, &buf)
Using io.Writer
tmpl, _ := template.Compile("Hello, {{ name }}!")
ctx := template.NewExecutionContext(template.Context{"name": "World"})
_ = tmpl.Execute(ctx, os.Stdout)

Template Syntax

Variables

Use {{ }} to output variables, with dot notation for nested properties:

{{ user.name }}
{{ user.address.city }}
{{ items.0 }}

See Variables Documentation for details.

Filters

Use pipe | to apply filters to variables, supporting chaining and arguments:

{{ name|upcase }}
{{ title|truncate:20 }}
{{ name|downcase|capitalize }}
{{ price|plus:10|times:2 }}

See Filters Documentation for details.

Conditionals
{% if score > 80 %}
    Excellent
{% elif score > 60 %}
    Pass
{% else %}
    Fail
{% endif %}
Loops
{% for item in items %}
    {{ item }}
{% endfor %}

{% for key, value in dict %}
    {{ key }}: {{ value }}
{% endfor %}

The loop variable is available inside loops:

Property Description
loop.Index Current index (starting from 0)
loop.Revindex Reverse index
loop.First Whether this is the first iteration
loop.Last Whether this is the last iteration
loop.Length Total length of the collection

Supports {% break %} and {% continue %} for loop control.

See Control Structure Documentation for details.

Comments
{# This content will not appear in the output #}
Layout & inheritance (Set-scoped)

Available when the template is loaded via NewHTMLSet or NewTextSet:

{# Include a partial #}
{% include "partials/header.html" %}
{% include "partials/card.html" with title="Hi" count=3 %}      {# pass variables #}
{% include "partials/card.html" with title="Hi" only %}         {# isolate context #}
{% include "partials/optional.html" if_exists %}                {# missing is no-op #}
{% include page.widget %}                                        {# dynamic path #}

{# Inherit from a parent template #}
{% extends "layouts/base.html" %}

{# Define/override blocks #}
{% block content %}
  {{ block.super }}             {# call the parent block's output #}
  <p>My override</p>
{% endblock content %}

{# Output literal template syntax, no interpolation #}
{% raw %}
  Literal: {{ not_a_variable }}  {% for x in items %}
{% endraw %}

{% include %}, {% extends %}, {% block %}, and {% raw %} are not available to template.Compile(src). They exist only in templates loaded through a Set. See docs/layout.md.

Auto-escape & safe (HTMLSet only)

NewHTMLSet auto-escapes every {{ expr }} output:

{{ page.title }}                 {# auto-escaped: < becomes &lt; #}
{{ page.content | safe }}        {# marked trusted — rendered as-is #}
{{ user_input | escape }}        {# explicit escape #}

Go code can mark values as trusted using template.SafeString:

set.Render("page.html", template.Context{
    "title":   "Hello <world>",                            // escaped
    "content": template.SafeString("<p>pre-rendered</p>"), // NOT escaped
}, w)

NewTextSet and template.Compile do not escape anything — they treat SafeString as a plain string.

Expressions

Supports the following operators (from lowest to highest precedence):

Operator Description
or, || Logical OR
and, && Logical AND
==, !=, <, >, <=, >= Comparison
+, - Addition, Subtraction
*, /, % Multiplication, Division, Modulo
not, -, + Unary operators

Literal support: strings ("text" / 'text'), numbers (42, 3.14), booleans (true / false), null (null).

Built-in Filters

String
Filter Description Example
default Return default value if empty {{ name|default:'Anonymous' }}
upcase Convert to uppercase {{ name|upcase }}
downcase Convert to lowercase {{ name|downcase }}
capitalize Capitalize first letter {{ name|capitalize }}
strip Remove leading/trailing whitespace {{ text|strip }}
lstrip Remove leading whitespace {{ text|lstrip }}
rstrip Remove trailing whitespace {{ text|rstrip }}
truncate Truncate to length (default 50) {{ text|truncate:20 }}
truncatewords Truncate to word count (default 15) {{ text|truncatewords:5 }}
replace Replace all occurrences {{ text|replace:'old','new' }}
replace_first Replace first occurrence {{ text|replace_first:'old','new' }}
replace_last Replace last occurrence {{ text|replace_last:'old','new' }}
remove Remove all occurrences {{ text|remove:'bad' }}
remove_first Remove first occurrence {{ text|remove_first:'x' }}
remove_last Remove last occurrence {{ text|remove_last:'x' }}
append Append string {{ name|append:'!' }}
prepend Prepend string {{ name|prepend:'Hi ' }}
split Split by delimiter {{ csv|split:',' }}
slice Extract substring by offset and length {{ text|slice:1,3 }}
escape Escape HTML characters {{ html|escape }}
escape_once Escape without double-escaping {{ html|escape_once }}
strip_html Remove HTML tags {{ html|strip_html }}
strip_newlines Remove newline characters {{ text|strip_newlines }}
url_encode Percent-encode for URLs {{ text|url_encode }}
url_decode Decode percent-encoded string {{ text|url_decode }}
base64_encode Encode to Base64 {{ text|base64_encode }}
base64_decode Decode from Base64 {{ text|base64_decode }}

String extensions (not in Liquid standard):

Filter Description Example
titleize Capitalize first letter of each word {{ title|titleize }}
camelize Convert to camelCase {{ name|camelize }}
pascalize Convert to PascalCase {{ name|pascalize }}
dasherize Convert to dash-separated {{ name|dasherize }}
slugify Convert to URL-friendly format {{ title|slugify }}
pluralize Singular/plural selection {{ count|pluralize:'item','items' }}
ordinalize Convert to ordinal {{ num|ordinalize }}
length Get string/array/map length {{ name|length }}
Math
Filter Description Example
plus Addition {{ price|plus:10 }}
minus Subtraction {{ price|minus:5 }}
times Multiplication {{ price|times:2 }}
divided_by Division {{ total|divided_by:3 }}
modulo Modulo {{ num|modulo:2 }}
abs Absolute value {{ num|abs }}
round Round (default precision 0) {{ pi|round:2 }}
floor Floor {{ num|floor }}
ceil Ceiling {{ num|ceil }}
at_least Ensure minimum value {{ num|at_least:0 }}
at_most Ensure maximum value {{ num|at_most:100 }}
Array
Filter Description Example
join Join with separator (default " ") {{ items|join:', ' }}
first First element {{ items|first }}
last Last element {{ items|last }}
size Collection or string length {{ items|size }}
reverse Reverse order {{ items|reverse }}
sort Sort elements {{ items|sort }}
sort_natural Case-insensitive sort {{ items|sort_natural }}
uniq Remove duplicates {{ items|uniq }}
compact Remove nil values {{ items|compact }}
concat Combine two arrays {{ items|concat:more }}
map Extract key from each element {{ users|map:'name' }}
where Select items matching key/value {{ users|where:'active','true' }}
reject Reject items matching key/value {{ users|reject:'active','false' }}
find Find first matching item {{ users|find:'name','Bob' }}
find_index Find index of first match {{ users|find_index:'name','Bob' }}
has Check if any item matches {{ users|has:'name','Alice' }}
sum Sum values (supports property) {{ scores|sum }}

Array extensions (not in Liquid standard):

Filter Description Example
shuffle Random shuffle {{ items|shuffle }}
random Random element {{ items|random }}
max Maximum value {{ scores|max }}
min Minimum value {{ scores|min }}
average Average {{ scores|average }}
Date
Filter Description Example
date Format date (PHP-style) {{ timestamp|date:'Y-m-d' }}
day Extract day {{ timestamp|day }}
month Extract month number {{ timestamp|month }}
month_full Full month name {{ timestamp|month_full }}
year Extract year {{ timestamp|year }}
week ISO week number {{ timestamp|week }}
weekday Day of week {{ timestamp|weekday }}
time_ago Relative time {{ timestamp|time_ago }}
Number Formatting
Filter Description Example
number Number formatting {{ price|number:'0.00' }}
bytes Convert to readable byte units {{ fileSize|bytes }}
Serialization
Filter Description Example
json Serialize to JSON {{ data|json }}
Map
Filter Description Example
extract Extract nested value by dot path {{ data|extract:'user.name' }}

Extension

Custom Filters
template.RegisterFilter("repeat", func(value any, args ...any) (any, error) {
    s := fmt.Sprintf("%v", value)
    n := 2
    if len(args) > 0 {
        if parsed, err := strconv.Atoi(fmt.Sprintf("%v", args[0])); err == nil {
            n = parsed
        }
    }
    return strings.Repeat(s, n), nil
})

// {{ "ha"|repeat:3 }} -> "hahaha"
Custom Tags

Register custom tags by implementing the Statement interface and calling RegisterTag. Here's an example of a {% set %} tag:

type SetNode struct {
    VarName    string
    Expression template.Expression
    Line, Col  int
}

func (n *SetNode) Position() (int, int) { return n.Line, n.Col }
func (n *SetNode) String() string       { return fmt.Sprintf("Set(%s)", n.VarName) }
func (n *SetNode) Execute(ctx *template.ExecutionContext, _ io.Writer) error {
    val, err := n.Expression.Evaluate(ctx)
    if err != nil {
        return err
    }
    ctx.Set(n.VarName, val.Interface())
    return nil
}

template.RegisterTag("set", func(doc *template.Parser, start *template.Token, arguments *template.Parser) (template.Statement, error) {
    varToken, err := arguments.ExpectIdentifier()
    if err != nil {
        return nil, arguments.Error("expected variable name after 'set'")
    }
    if arguments.Match(template.TokenSymbol, "=") == nil {
        return nil, arguments.Error("expected '=' after variable name")
    }
    expr, err := arguments.ParseExpression()
    if err != nil {
        return nil, err
    }
    return &SetNode{
        VarName:    varToken.Value,
        Expression: expr,
        Line:       start.Line,
        Col:        start.Col,
    }, nil
})

Examples

Runnable examples live in the examples/ directory:

Directory Demonstrates
examples/usage Single-string Compile / Render basics
examples/custom_filters Registering a custom filter via RegisterFilter
examples/custom_tags Registering a custom tag via RegisterTag
examples/layout Multi-file HTML site with {% extends %} / {% block %} / {% include %} / block.super / auto-escape
examples/multifile_text Code generation with NewTextSet (no HTML escape) and multi-file includes

Run any example with go run ./examples/<name>.

Context Building

// Using map directly
output, _ := template.Render(source, map[string]any{
    "name": "Alice",
    "age":  30,
})

// Using ContextBuilder (supports struct expansion)
ctx, err := template.NewContextBuilder().
    KeyValue("name", "Alice").
    Struct(user).
    Build()
output, _ := tmpl.Render(ctx)

Error Reporting

All errors include precise line and column position information:

lexer error at line 1, col 7: unclosed variable tag, expected '}}'
parse error at line 1, col 4: unknown tag: unknown
parse error at line 1, col 19: unexpected EOF, expected one of: [elif else endif]

Liquid Compatibility

This engine targets compatibility with the Liquid template standard. Filter names follow Liquid conventions (upcase, downcase, strip, at_least, divided_by, etc.).

For a complete comparison including behavioral differences, missing filters, extension filters, and convenience aliases, see Liquid Compatibility.

Architecture

See ARCHITECTURE.md for details.

Contributing

Contributions are welcome. Please see Contributing Guide.

License

MIT License - see LICENSE for details.

Documentation

Overview

Package template provides a lightweight template engine with Django/Jinja-style syntax.

Package template provides a simple and efficient template engine for Go.

Single-string usage (backwards-compatible, minimal feature set):

tmpl, err := template.Compile("Hello, {{ name|upper }}!")
if err != nil {
	panic(err)
}

output, err := tmpl.Render(template.Context{"name": "world"})
// Output: "Hello, WORLD!"

Multi-file usage (layout, inheritance, includes, HTML auto-escape):

loader, _ := template.NewDirLoader("./templates")
set := template.NewHTMLSet(loader,
	template.WithGlobals(template.Context{"site": siteData}),
)
_ = set.Render("layouts/blog.html", template.Context{"page": pageData}, os.Stdout)

Two worlds, one package:

  • Compile / Render — simple string templates. Supports {{ var }}, filters, {% if %}, {% for %}, {% break %}, {% continue %}, {# ... #}. Does NOT support {% include %}, {% extends %}, {% block %}, {% raw %}, or the "safe" filter. Does NOT auto-escape. Suited to log lines, config snippets, plain-text emails, and anything that is "interpolate a few values into a string".

  • NewTextSet / NewHTMLSet — multi-file template systems with Loader-backed inclusion, inheritance, block.super, raw blocks, and (HTMLSet only) automatic HTML escaping with the SafeString mechanism. Layout tags and the safe filter live in a per-Set registry so the Compile(src) path above is unaffected.

This mirrors Go's own text/template vs html/template split.

Supported syntax summary:

  • Variable interpolation: {{ variable }}
  • Filters: {{ variable | filter:arg }}
  • Control structures: {% if %}, {% for %}
  • Comments: {# ... #}
  • (Set only) {% include "x" [with k=v] [only] [if_exists] %}
  • (Set only) {% extends "parent" %} + {% block name %}...{% endblock %}
  • (Set only) {{ block.super }}
  • (Set only) {% raw %}...{% endraw %}
  • (Set only) {{ x | safe }} and (HTMLSet only) auto-escape of {{ x }}

Architecture:

The package is organized into several key components:

  • Lexer: Tokenizes template source into a token stream
  • Parser: Converts tokens into an AST (Abstract Syntax Tree)
  • Expression Parser: Parses expressions with operator precedence
  • Template: Executes the AST with a given context
  • Filters: Transforms values during template execution
  • Context: Stores and retrieves template variables

Control Flow:

The template engine supports break and continue statements within loops:

{% for item in items %}
	{% if item == "skip" %}
		{% continue %}
	{% endif %}
	{% if item == "stop" %}
		{% break %}
	{% endif %}
	{{ item }}
{% endfor %}

Loop Context:

Within loops, a special "loop" variable is available providing:

  • loop.Index: Current index (0-based)
  • loop.Revindex: Reverse index
  • loop.First: True if first iteration
  • loop.Last: True if last iteration
  • loop.Length: Total collection length

For detailed examples, see the examples/ directory.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTemplateNotFound      = errors.New("template not found")
	ErrInvalidTemplateName   = errors.New("invalid template name")
	ErrIncludeDepthExceeded  = errors.New("include depth exceeded")
	ErrExtendsDepthExceeded  = errors.New("extends depth exceeded")
	ErrCircularExtends       = errors.New("circular extends detected")
	ErrExtendsNotFirst       = errors.New("extends must be the first tag")
	ErrExtendsPathNotLiteral = errors.New("extends path must be a string literal")
	ErrBlockRedefined        = errors.New("block redefined in same template")
	ErrUnclosedRaw           = errors.New("unclosed raw block")
	ErrIncludePathNotString  = errors.New("include path did not evaluate to string")
)

ErrTemplateNotFound indicates a loader could not locate the named template. ErrInvalidTemplateName indicates the template name failed fs.ValidPath validation (contains "..", is absolute, contains NUL, backslash, or similar). ErrIncludeDepthExceeded indicates include nesting exceeded the hard limit. ErrExtendsDepthExceeded indicates the extends chain exceeded the hard limit. ErrCircularExtends indicates a cycle was detected in the extends chain. ErrExtendsNotFirst indicates an extends tag appeared after other content. ErrExtendsPathNotLiteral indicates an extends path was an expression, not a string literal. ErrBlockRedefined indicates the same block name appeared twice in one template. ErrUnclosedRaw indicates a raw block was not terminated with endraw. ErrIncludePathNotString indicates a dynamic include path evaluated to a non-string value.

View Source
var (
	ErrContextKeyNotFound     = errors.New("key not found in context")
	ErrContextInvalidKeyType  = errors.New("invalid key type for navigation")
	ErrContextIndexOutOfRange = errors.New("index out of range in context")
)

ErrContextKeyNotFound indicates a key was not found in the execution context. ErrContextInvalidKeyType indicates an invalid key type during context navigation. ErrContextIndexOutOfRange indicates an index out of range during context navigation.

View Source
var (
	ErrFilterNotFound               = errors.New("filter not found")
	ErrFilterExecutionFailed        = errors.New("filter execution failed")
	ErrFilterInputInvalid           = errors.New("filter input is invalid")
	ErrFilterArgsInvalid            = errors.New("filter arguments are invalid")
	ErrFilterInputEmpty             = errors.New("filter input is empty")
	ErrFilterInputNotSlice          = errors.New("filter input is not a slice")
	ErrFilterInputNotNumeric        = errors.New("filter input is not numeric")
	ErrFilterInputInvalidTimeFormat = errors.New("filter input has an invalid time format")
	ErrFilterInputUnsupportedType   = errors.New("filter input is of an unsupported type")
	ErrInsufficientArgs             = errors.New("insufficient arguments provided")
	ErrInvalidFilterName            = errors.New("invalid filter name")
	ErrUnknownFilterArgumentType    = errors.New("unknown argument type")
	ErrExpectedFilterName           = errors.New("expected filter name after '|'")
)

ErrFilterNotFound indicates a referenced filter does not exist. ErrFilterExecutionFailed indicates a filter failed during execution. ErrFilterInputInvalid indicates the filter received invalid input. ErrFilterArgsInvalid indicates the filter received invalid arguments. ErrFilterInputEmpty indicates the filter received empty input. ErrFilterInputNotSlice indicates the filter expected a slice input. ErrFilterInputNotNumeric indicates the filter expected numeric input. ErrFilterInputInvalidTimeFormat indicates the filter received an invalid time format. ErrFilterInputUnsupportedType indicates the filter received an unsupported input type. ErrInsufficientArgs indicates insufficient arguments were provided to a filter. ErrInvalidFilterName indicates an invalid filter name was used. ErrUnknownFilterArgumentType indicates an unknown argument type was passed to a filter. ErrExpectedFilterName indicates a filter name was expected after the pipe operator.

View Source
var (
	ErrUnexpectedCharacter = errors.New("unexpected character")
	ErrUnterminatedString  = errors.New("unterminated string literal")
)

ErrUnexpectedCharacter indicates the lexer encountered an unexpected character. ErrUnterminatedString indicates a string literal was not properly closed.

View Source
var (
	ErrInvalidNumber   = errors.New("invalid number")
	ErrExpectedRParen  = errors.New("expected ')'")
	ErrUnexpectedToken = errors.New("unexpected token")
	ErrUnknownNodeType = errors.New("unknown node type")
	ErrIntegerOverflow = errors.New("unsigned integer value exceeds maximum int64 value")
)

ErrInvalidNumber indicates the parser encountered an invalid numeric literal. ErrExpectedRParen indicates a closing parenthesis was expected but not found. ErrUnexpectedToken indicates the parser encountered an unexpected token. ErrUnknownNodeType indicates an unknown AST node type was encountered. ErrIntegerOverflow indicates an unsigned integer value exceeds the maximum int64 value.

View Source
var (
	ErrUnsupportedType     = errors.New("unsupported type")
	ErrUnsupportedOperator = errors.New("unsupported operator")
	ErrUnsupportedUnaryOp  = errors.New("unsupported unary operator")
)

ErrUnsupportedType indicates an unsupported type was encountered. ErrUnsupportedOperator indicates an unsupported operator was used. ErrUnsupportedUnaryOp indicates an unsupported unary operator was used.

View Source
var (
	ErrUndefinedVariable     = errors.New("undefined variable")
	ErrUndefinedProperty     = errors.New("undefined property")
	ErrNonStructProperty     = errors.New("cannot access property of non-struct value")
	ErrCannotAccessProperty  = errors.New("cannot access property")
	ErrNonObjectProperty     = errors.New("cannot access property of non-object")
	ErrInvalidVariableAccess = errors.New("invalid variable access")
)

ErrUndefinedVariable indicates a referenced variable is not defined. ErrUndefinedProperty indicates a referenced property is not defined. ErrNonStructProperty indicates a property access was attempted on a non-struct value. ErrCannotAccessProperty indicates a property cannot be accessed. ErrNonObjectProperty indicates a property access was attempted on a non-object value. ErrInvalidVariableAccess indicates an invalid variable access pattern.

View Source
var (
	ErrCannotAddTypes       = errors.New("cannot add values of these types")
	ErrCannotSubtractTypes  = errors.New("cannot subtract values of these types")
	ErrCannotMultiplyTypes  = errors.New("cannot multiply values of these types")
	ErrCannotDivideTypes    = errors.New("cannot divide values of these types")
	ErrCannotModuloTypes    = errors.New("cannot modulo values of these types")
	ErrDivisionByZero       = errors.New("division by zero")
	ErrModuloByZero         = errors.New("modulo by zero")
	ErrCannotConvertToBool  = errors.New("cannot convert type to boolean")
	ErrCannotNegate         = errors.New("cannot negate value")
	ErrCannotApplyUnaryPlus = errors.New("cannot apply unary plus")
	ErrCannotCompareTypes   = errors.New("cannot compare values of these types")
)

ErrCannotAddTypes indicates addition is not supported for the given types. ErrCannotSubtractTypes indicates subtraction is not supported for the given types. ErrCannotMultiplyTypes indicates multiplication is not supported for the given types. ErrCannotDivideTypes indicates division is not supported for the given types. ErrCannotModuloTypes indicates modulo is not supported for the given types. ErrDivisionByZero indicates a division by zero was attempted. ErrModuloByZero indicates a modulo by zero was attempted. ErrCannotConvertToBool indicates a value cannot be converted to boolean. ErrCannotNegate indicates a value cannot be negated. ErrCannotApplyUnaryPlus indicates unary plus cannot be applied to the value. ErrCannotCompareTypes indicates comparison is not supported for the given types.

View Source
var (
	ErrInvalidIndexType      = errors.New("invalid index type")
	ErrInvalidArrayIndex     = errors.New("invalid array index")
	ErrIndexOutOfRange       = errors.New("index out of range")
	ErrCannotIndexNil        = errors.New("cannot index nil")
	ErrTypeNotIndexable      = errors.New("type is not indexable")
	ErrCannotGetKeyFromNil   = errors.New("cannot get key from nil")
	ErrTypeNotMap            = errors.New("type is not a map")
	ErrCannotGetFieldFromNil = errors.New("cannot get field from nil")
	ErrStructHasNoField      = errors.New("struct has no field")
	ErrTypeHasNoField        = errors.New("type has no field")
	ErrUnsupportedArrayType  = errors.New("unsupported array type")
)

ErrInvalidIndexType indicates an invalid index type was used. ErrInvalidArrayIndex indicates an invalid array index was used. ErrIndexOutOfRange indicates an index is out of range. ErrCannotIndexNil indicates an indexing operation was attempted on nil. ErrTypeNotIndexable indicates the type does not support indexing. ErrCannotGetKeyFromNil indicates a key lookup was attempted on nil. ErrTypeNotMap indicates the type is not a map. ErrCannotGetFieldFromNil indicates a field access was attempted on nil. ErrStructHasNoField indicates the struct does not have the requested field. ErrTypeHasNoField indicates the type does not have the requested field. ErrUnsupportedArrayType indicates an unsupported array type was encountered.

View Source
var (
	ErrUnsupportedCollectionType = errors.New("unsupported collection type for for loop")
	ErrTypeNotIterable           = errors.New("type is not iterable")
	ErrTypeHasNoLength           = errors.New("type has no length")
)

ErrUnsupportedCollectionType indicates the collection type is not supported in a for loop. ErrTypeNotIterable indicates the type does not support iteration. ErrTypeHasNoLength indicates the type does not support the length operation.

View Source
var (
	ErrCannotConvertNilToInt   = errors.New("cannot convert nil to int")
	ErrCannotConvertToInt      = errors.New("cannot convert value to int")
	ErrCannotConvertNilToFloat = errors.New("cannot convert nil to float")
	ErrCannotConvertToFloat    = errors.New("cannot convert value to float")
	ErrExpectedSliceOrArray    = errors.New("expected slice or array")
)

ErrCannotConvertNilToInt indicates nil cannot be converted to int. ErrCannotConvertToInt indicates the value cannot be converted to int. ErrCannotConvertNilToFloat indicates nil cannot be converted to float. ErrCannotConvertToFloat indicates the value cannot be converted to float.

View Source
var (
	ErrBreakOutsideLoop    = errors.New("break statement outside of loop")
	ErrContinueOutsideLoop = errors.New("continue statement outside of loop")
)

ErrBreakOutsideLoop indicates a break statement was used outside of a loop. ErrContinueOutsideLoop indicates a continue statement was used outside of a loop.

View Source
var (
	ErrTagAlreadyRegistered = errors.New("tag already registered")

	ErrMultipleElseStatements         = errors.New("multiple 'else' statements found in if block, use 'elif' for additional conditions")
	ErrUnexpectedTokensAfterCondition = errors.New("unexpected tokens after condition")
	ErrElseNoArgs                     = errors.New("else does not take arguments")
	ErrEndifNoArgs                    = errors.New("endif does not take arguments")
	ErrElifAfterElse                  = errors.New("elif cannot appear after else")

	ErrExpectedVariable                = errors.New("expected variable name")
	ErrExpectedSecondVariable          = errors.New("expected second variable name after comma")
	ErrExpectedInKeyword               = errors.New("expected 'in' keyword")
	ErrUnexpectedTokensAfterCollection = errors.New("unexpected tokens after collection")
	ErrEndforNoArgs                    = errors.New("endfor does not take arguments")

	ErrBreakNoArgs    = errors.New("break does not take arguments")
	ErrContinueNoArgs = errors.New("continue does not take arguments")
)

ErrTagAlreadyRegistered indicates a tag with the same name is already registered.

ErrMultipleElseStatements indicates multiple else clauses were found in an if block. ErrUnexpectedTokensAfterCondition indicates unexpected tokens after a condition expression. ErrElseNoArgs indicates the else tag received unexpected arguments. ErrEndifNoArgs indicates the endif tag received unexpected arguments. ErrElifAfterElse indicates an elif clause appeared after an else clause.

ErrExpectedVariable indicates a variable name was expected but not found. ErrExpectedSecondVariable indicates a second variable name was expected after a comma. ErrExpectedInKeyword indicates the "in" keyword was expected but not found. ErrUnexpectedTokensAfterCollection indicates unexpected tokens after a collection expression. ErrEndforNoArgs indicates the endfor tag received unexpected arguments.

ErrBreakNoArgs indicates the break tag received unexpected arguments. ErrContinueNoArgs indicates the continue tag received unexpected arguments.

Functions

func HasFilter added in v0.4.0

func HasFilter(name string) bool

HasFilter reports whether the default registry contains a filter with the given name.

func HasTag added in v0.4.0

func HasTag(name string) bool

HasTag checks if a tag with the given name is registered in the global registry. It is safe to call from multiple goroutines.

func IsKeyword added in v0.4.0

func IsKeyword(ident string) bool

IsKeyword reports whether ident is a reserved keyword.

func IsSymbol added in v0.4.0

func IsSymbol(s string) bool

IsSymbol reports whether s is a valid operator or punctuation symbol.

func ListFilters added in v0.4.0

func ListFilters() []string

ListFilters returns a sorted list of all filter names in the default registry.

func ListTags added in v0.4.0

func ListTags() []string

ListTags returns a sorted list of all tag names in the global registry. It is safe to call from multiple goroutines.

func RegisterFilter

func RegisterFilter(name string, fn FilterFunc)

RegisterFilter registers a filter in the default registry. It panics if fn is nil.

func RegisterTag added in v0.4.0

func RegisterTag(name string, parser TagParser) error

RegisterTag registers a tag parser in the global registry. It is safe to call from multiple goroutines.

func Render

func Render(source string, data Context) (string, error)

Render compiles and renders a template in one step.

Render is a shorthand for calling Compile followed by Template.Render. For repeated rendering of the same template, compile once with Compile and call Template.Render to avoid redundant compilation.

func UnregisterFilter added in v0.4.0

func UnregisterFilter(name string)

UnregisterFilter removes a filter from the default registry.

func UnregisterTag added in v0.4.0

func UnregisterTag(name string)

UnregisterTag removes a tag from the global registry. It is safe to call from multiple goroutines.

func ValidateName added in v0.6.0

func ValidateName(name string) error

ValidateName checks that name is safe to use as a template path. It rejects:

  • anything fs.ValidPath rejects (empty element, "..", absolute, trailing /)
  • backslash (Windows path separator; forces forward-slash discipline)
  • NUL byte (path injection)

Loader implementations must call this on every name they receive.

Types

type BinaryOpNode added in v0.4.0

type BinaryOpNode struct {
	Operator string
	Left     Expression
	Right    Expression
	Line     int
	Col      int
}

BinaryOpNode represents a binary operation.

func NewBinaryOpNode added in v0.4.0

func NewBinaryOpNode(operator string, left, right Expression, line, col int) *BinaryOpNode

NewBinaryOpNode returns a new BinaryOpNode.

func (*BinaryOpNode) Evaluate added in v0.4.0

func (n *BinaryOpNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate computes the binary operation result.

func (*BinaryOpNode) Position added in v0.4.0

func (n *BinaryOpNode) Position() (int, int)

Position returns the position of the BinaryOpNode.

func (*BinaryOpNode) String added in v0.4.0

func (n *BinaryOpNode) String() string

String returns a debug representation of the BinaryOpNode.

type BlockNode added in v0.6.0

type BlockNode struct {
	Name string
	Body []Node
	Line int
	Col  int
}

BlockNode is a forward declaration stub filled in when {% block %} is implemented. Defined here so Template.blocks can reference it without import cycles.

func (*BlockNode) Execute added in v0.6.0

func (n *BlockNode) Execute(ctx *ExecutionContext, w io.Writer) error

Execute renders this block, resolving overrides across the extends chain. If the current template is not part of a chain (or this block is inside an included partial), the block simply renders its own body inline.

The {{ block.super }} expression is supported by injecting a "block" variable into the execution context whose "super" field is the pre-rendered parent block body (already a SafeString so it survives HTML auto-escape untouched).

func (*BlockNode) Position added in v0.6.0

func (n *BlockNode) Position() (int, int)

Position returns the source position of the block.

func (*BlockNode) String added in v0.6.0

func (n *BlockNode) String() string

String returns a debug representation.

type BreakError added in v0.4.0

type BreakError struct{}

BreakError signals loop termination.

func (*BreakError) Error added in v0.4.0

func (e *BreakError) Error() string

Error implements the error interface.

type BreakNode added in v0.4.0

type BreakNode struct {
	Line int
	Col  int
}

BreakNode represents a {% break %} statement.

func (*BreakNode) Execute added in v0.4.0

func (n *BreakNode) Execute(_ *ExecutionContext, _ io.Writer) error

Execute signals loop termination via BreakError.

func (*BreakNode) Position added in v0.4.0

func (n *BreakNode) Position() (int, int)

Position returns the position of the BreakNode.

func (*BreakNode) String added in v0.4.0

func (n *BreakNode) String() string

String returns a debug representation of the BreakNode.

type Context

type Context map[string]any

Context stores template variables as a string-keyed map. Values can be of any type. Dot-notation (e.g., "user.name") is supported for nested access.

func NewContext

func NewContext() Context

NewContext creates and returns a new empty Context.

func (Context) Get

func (c Context) Get(key string) (any, error)

Get retrieves a value from the Context by key. Dot-separated keys (e.g., "user.profile.name") navigate nested structures. Array indices are supported (e.g., "items.0").

Get returns ErrContextKeyNotFound, ErrContextIndexOutOfRange, or ErrContextInvalidKeyType on failure.

func (Context) Set

func (c Context) Set(key string, value any)

Set inserts a value into the Context with the specified key. Dot-notation (e.g., "user.address.city") creates nested map structures. Top-level keys preserve original data types; nested keys use map[string]any. Empty keys are silently ignored.

type ContextBuilder added in v0.3.6

type ContextBuilder struct {
	// contains filtered or unexported fields
}

ContextBuilder provides a fluent API for building a Context with error collection.

func NewContextBuilder added in v0.3.6

func NewContextBuilder() *ContextBuilder

NewContextBuilder creates a new ContextBuilder for fluent Context construction.

ctx, err := NewContextBuilder().
    KeyValue("name", "John").
    Struct(user).
    Build()

func (*ContextBuilder) Build added in v0.3.6

func (cb *ContextBuilder) Build() (Context, error)

Build returns the constructed Context and any collected errors. Errors from ContextBuilder.KeyValue or ContextBuilder.Struct operations are joined into a single error.

func (*ContextBuilder) KeyValue added in v0.3.6

func (cb *ContextBuilder) KeyValue(key string, value any) *ContextBuilder

KeyValue sets a key-value pair and returns the builder for chaining.

builder := NewContextBuilder().
    KeyValue("name", "John").
    KeyValue("age", 30)

func (*ContextBuilder) Struct added in v0.3.6

func (cb *ContextBuilder) Struct(v any) *ContextBuilder

Struct expands struct fields into the Context using JSON serialization. Fields are flattened to top-level keys based on their json tags. Nested structs are preserved as nested maps accessible via dot notation. If serialization fails, the error is collected and returned by ContextBuilder.Build.

type ContinueError added in v0.4.0

type ContinueError struct{}

ContinueError signals loop continuation.

func (*ContinueError) Error added in v0.4.0

func (e *ContinueError) Error() string

Error implements the error interface.

type ContinueNode added in v0.4.0

type ContinueNode struct {
	Line int
	Col  int
}

ContinueNode represents a {% continue %} statement.

func (*ContinueNode) Execute added in v0.4.0

func (n *ContinueNode) Execute(_ *ExecutionContext, _ io.Writer) error

Execute signals loop continuation via ContinueError.

func (*ContinueNode) Position added in v0.4.0

func (n *ContinueNode) Position() (int, int)

Position returns the position of the ContinueNode.

func (*ContinueNode) String added in v0.4.0

func (n *ContinueNode) String() string

String returns a debug representation of the ContinueNode.

type ExecutionContext added in v0.4.0

type ExecutionContext struct {
	Public  Context // user-provided variables
	Private Context // internal variables (e.g., loop counters)
	// contains filtered or unexported fields
}

ExecutionContext holds the execution state for template rendering, separating user-provided variables (Public) from internal variables (Private).

func NewChildContext added in v0.4.0

func NewChildContext(parent *ExecutionContext) *ExecutionContext

NewChildContext creates a child ExecutionContext that shares the parent's Public context but copies the Private context for isolated scope.

func NewExecutionContext added in v0.4.0

func NewExecutionContext(data Context) *ExecutionContext

NewExecutionContext creates a new ExecutionContext from user data.

func (*ExecutionContext) Get added in v0.4.0

func (ec *ExecutionContext) Get(name string) (any, bool)

Get retrieves a variable, checking Private first, then Public.

func (*ExecutionContext) Set added in v0.4.0

func (ec *ExecutionContext) Set(name string, value any)

Set stores a variable in the private context.

type ExprParser added in v0.4.0

type ExprParser struct {
	// contains filtered or unexported fields
}

ExprParser parses expressions from a token stream. It handles operator precedence, filters, property access, etc.

func NewExprParser added in v0.4.0

func NewExprParser(tokens []*Token) *ExprParser

NewExprParser creates a new expression parser.

func (*ExprParser) ParseExpression added in v0.4.0

func (p *ExprParser) ParseExpression() (Expression, error)

ParseExpression parses a complete expression. This is the entry point for expression parsing.

type Expression added in v0.4.0

type Expression interface {
	Node
	Evaluate(ctx *ExecutionContext) (*Value, error)
}

Expression is the interface for all expression nodes. Expressions are evaluated to produce values.

type ExtendsNode added in v0.6.0

type ExtendsNode struct {
	Line int
	Col  int
}

ExtendsNode is a marker for {% extends "parent" %}. It produces no output directly; inheritance is handled by Template.Execute when it detects a non-nil parent field.

func (*ExtendsNode) Execute added in v0.6.0

func (n *ExtendsNode) Execute(_ *ExecutionContext, _ io.Writer) error

Execute is a no-op. The parent relationship is established at parse time and consumed by Template.Execute.

func (*ExtendsNode) Position added in v0.6.0

func (n *ExtendsNode) Position() (int, int)

Position returns the source position of the extends tag.

func (*ExtendsNode) String added in v0.6.0

func (n *ExtendsNode) String() string

String returns a debug representation.

type FilterFunc

type FilterFunc func(value any, args ...any) (any, error)

FilterFunc represents the signature of functions that can be applied as filters.

func Filter

func Filter(name string) (FilterFunc, bool)

Filter retrieves a filter from the default registry.

type FilterNode added in v0.4.0

type FilterNode struct {
	Expr Expression
	Name string
	Args []Expression
	Line int
	Col  int
}

FilterNode represents a filter application.

func NewFilterNode added in v0.4.0

func NewFilterNode(expr Expression, name string, args []Expression, line, col int) *FilterNode

NewFilterNode returns a new FilterNode.

func (*FilterNode) Evaluate added in v0.4.0

func (n *FilterNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate applies the named filter to the expression value.

Filter lookup consults the per-Set filter registry first (if the template was loaded via a Set), falling back to the global registry. This gives Set-loaded templates access to the safe filter and the HTML-aware escape variants without exposing them to Compile(src).

func (*FilterNode) Position added in v0.4.0

func (n *FilterNode) Position() (int, int)

Position returns the position of the FilterNode.

func (*FilterNode) String added in v0.4.0

func (n *FilterNode) String() string

String returns a debug representation of the FilterNode.

type ForNode added in v0.4.0

type ForNode struct {
	Vars       []string
	Collection Expression
	Body       []Node
	Line       int
	Col        int
}

ForNode represents a for loop.

func (*ForNode) Execute added in v0.4.0

func (n *ForNode) Execute(ctx *ExecutionContext, w io.Writer) error

Execute evaluates the iterable and executes the loop body for each element.

func (*ForNode) Position added in v0.4.0

func (n *ForNode) Position() (int, int)

Position returns the position of the ForNode.

func (*ForNode) String added in v0.4.0

func (n *ForNode) String() string

String returns a debug representation of the ForNode.

type IfBranch added in v0.4.0

type IfBranch struct {
	Condition Expression
	Body      []Node
}

IfBranch represents a single if or elif branch.

type IfNode added in v0.4.0

type IfNode struct {
	Branches []IfBranch
	ElseBody []Node
	Line     int
	Col      int
}

IfNode represents an if-elif-else conditional block.

func (*IfNode) Execute added in v0.4.0

func (n *IfNode) Execute(ctx *ExecutionContext, w io.Writer) error

Execute runs the first truthy branch, or the else block if no branch matches.

func (*IfNode) Position added in v0.4.0

func (n *IfNode) Position() (int, int)

Position returns the position of the IfNode.

func (*IfNode) String added in v0.4.0

func (n *IfNode) String() string

String returns a debug representation of the IfNode.

type IncludeNode added in v0.6.0

type IncludeNode struct {
	Line int
	Col  int
	// contains filtered or unexported fields
}

IncludeNode represents an {% include %} statement.

Shapes:

  • Static path (string literal), resolved at parse time: prepared != nil.
  • Parse-time circular static path: lazy = true, staticName holds the literal for runtime lookup.
  • Dynamic path (expression): lazy = true, pathExpr != nil.

Options:

  • withPairs: {% include "x" with k1=expr1 k2=expr2 %}
  • only: {% include "x" only %} — fully isolates the child context, excluding parent variables AND globals.
  • ifExists: {% include "x" if_exists %} — missing template is a no-op instead of an error.

func (*IncludeNode) Execute added in v0.6.0

func (n *IncludeNode) Execute(ctx *ExecutionContext, w io.Writer) error

Execute renders the included template. The parser only produces IncludeNode inside a Set, so ctx.set is guaranteed non-nil by the time we reach here.

func (*IncludeNode) Position added in v0.6.0

func (n *IncludeNode) Position() (int, int)

Position returns the source position of the include tag.

func (*IncludeNode) String added in v0.6.0

func (n *IncludeNode) String() string

String returns a debug representation.

type Lexer added in v0.2.0

type Lexer struct {
	// contains filtered or unexported fields
}

Lexer performs lexical analysis on template input.

func NewLexer added in v0.4.0

func NewLexer(input string) *Lexer

NewLexer creates a new Lexer for the given input.

func (*Lexer) Tokenize added in v0.4.0

func (l *Lexer) Tokenize() ([]*Token, error)

Tokenize performs lexical analysis and returns all tokens.

type LexerError added in v0.4.0

type LexerError struct {
	Message string
	Line    int
	Col     int
}

LexerError represents a lexical analysis error with position information.

func (*LexerError) Error added in v0.4.0

func (e *LexerError) Error() string

Error implements the error interface.

type LiteralNode added in v0.4.0

type LiteralNode struct {
	Value any
	Line  int
	Col   int
}

LiteralNode represents a literal value (string, number, boolean).

func NewLiteralNode added in v0.4.0

func NewLiteralNode(value any, line, col int) *LiteralNode

NewLiteralNode returns a new LiteralNode.

func (*LiteralNode) Evaluate added in v0.4.0

func (n *LiteralNode) Evaluate(_ *ExecutionContext) (*Value, error)

Evaluate returns the literal value wrapped in a Value.

func (*LiteralNode) Position added in v0.4.0

func (n *LiteralNode) Position() (int, int)

Position returns the position of the LiteralNode.

func (*LiteralNode) String added in v0.4.0

func (n *LiteralNode) String() string

String returns a debug representation of the LiteralNode.

type Loader added in v0.6.0

type Loader interface {
	Open(name string) (source string, resolved string, err error)
}

Loader locates and loads template source code by name.

Implementations must validate the name with ValidateName (which adds backslash and NUL rejection on top of fs.ValidPath) and return ErrInvalidTemplateName for any path that fails the check. Unknown names must return ErrTemplateNotFound.

The returned resolved name is used as the cache key and should be stable and unique within a loader (e.g., include a layer prefix when chained).

func NewChainLoader added in v0.6.0

func NewChainLoader(loaders ...Loader) Loader

NewChainLoader returns a Loader that queries the given loaders in order and returns the first one that has the requested template.

Chain loaders are typically used to implement override layers like user > theme > builtin. Each hit's resolved name is prefixed with the layer index so the same name in different layers produces distinct cache keys in a Set.

func NewDirLoader added in v0.6.0

func NewDirLoader(dir string) (Loader, error)

NewDirLoader returns a Loader that reads templates from the given local directory, sandboxed by os.Root. Symbolic links cannot escape the root: following any link whose target lies outside dir results in an error.

This is the default, recommended way to load templates from disk.

For development workflows that deliberately require symlink following (theme dev, monorepo sharing), use NewFSLoader with os.DirFS and accept responsibility for the relaxed sandbox.

func NewFSLoader added in v0.6.0

func NewFSLoader(fsys fs.FS) Loader

NewFSLoader wraps any fs.FS as a Loader. Intended for already- sandboxed filesystems such as embed.FS, testing/fstest.MapFS, and archive/zip.Reader.

Warning: if you pass a non-sandboxed fs.FS (for example os.DirFS pointing at a real directory) the library cannot prevent symbolic links from escaping. Prefer NewDirLoader for local directories unless you deliberately need this escape hatch.

func NewMemoryLoader added in v0.6.0

func NewMemoryLoader(files map[string]string) Loader

NewMemoryLoader returns a Loader that serves templates from an in-memory map. Intended for tests and small pre-registered sets.

type LoopContext added in v0.2.8

type LoopContext struct {
	Index      int
	Counter    int
	Revindex   int
	Revcounter int
	First      bool
	Last       bool
	Length     int
	Parent     *LoopContext
}

LoopContext represents loop metadata for templates.

type Node

type Node interface {
	// Position returns the line and column where this node starts.
	Position() (line, col int)

	// String returns a string representation of the node for debugging.
	String() string
}

Node is the interface that all AST nodes must implement. Each node represents a part of the template syntax tree.

type OutputNode added in v0.4.0

type OutputNode struct {
	Expr Expression
	Line int
	Col  int
}

OutputNode represents a variable output {{ ... }}.

func NewOutputNode added in v0.4.0

func NewOutputNode(expr Expression, line, col int) *OutputNode

NewOutputNode returns a new OutputNode.

func (*OutputNode) Execute added in v0.4.0

func (n *OutputNode) Execute(ctx *ExecutionContext, w io.Writer) error

Execute evaluates the expression and writes its string value.

When the execution context has autoescape enabled (HTMLSet rendering), the output is HTML-escaped UNLESS the underlying Go value is a SafeString. In the non-autoescape path (TextSet or standalone Compile), SafeString is treated as a plain string.

func (*OutputNode) Position added in v0.4.0

func (n *OutputNode) Position() (int, int)

Position returns the position of the OutputNode.

func (*OutputNode) String added in v0.4.0

func (n *OutputNode) String() string

String returns a debug representation of the OutputNode.

type ParseError added in v0.4.0

type ParseError struct {
	Message string
	Line    int
	Col     int
}

ParseError represents a parsing error with source location.

func (*ParseError) Error added in v0.4.0

func (e *ParseError) Error() string

Error returns a human-readable error message with source location.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser consumes tokens and builds an AST.

func NewParser

func NewParser(tokens []*Token) *Parser

NewParser creates a new Parser.

func (*Parser) Advance added in v0.4.0

func (p *Parser) Advance()

Advance moves to the next token.

func (*Parser) Current added in v0.4.0

func (p *Parser) Current() *Token

Current returns the current token without advancing the parser.

func (*Parser) Error added in v0.4.0

func (p *Parser) Error(msg string) error

Error creates a parse error at the current token position.

func (*Parser) Errorf added in v0.4.0

func (p *Parser) Errorf(format string, args ...any) error

Errorf creates a formatted parse error at the current token position.

func (*Parser) ExpectIdentifier added in v0.4.0

func (p *Parser) ExpectIdentifier() (*Token, error)

ExpectIdentifier expects and consumes an identifier token.

func (*Parser) Match added in v0.4.0

func (p *Parser) Match(tokenType TokenType, value string) *Token

Match consumes and returns the current token if type and value match. It returns nil when there is no match.

func (*Parser) Parse

func (p *Parser) Parse() ([]Statement, error)

Parse parses the entire template and returns AST statement nodes.

func (*Parser) ParseExpression added in v0.4.0

func (p *Parser) ParseExpression() (Expression, error)

ParseExpression parses an expression from the current token position.

func (*Parser) ParseUntil added in v0.4.0

func (p *Parser) ParseUntil(endTags ...string) ([]Statement, string, error)

ParseUntil parses nodes until one of the given end tags is encountered.

Returns the parsed nodes, the matched end-tag name, and any error.

func (*Parser) ParseUntilWithArgs added in v0.4.0

func (p *Parser) ParseUntilWithArgs(endTags ...string) ([]Statement, string, *Parser, error)

ParseUntilWithArgs parses nodes until one of the given end tags is encountered, and also returns a parser for the end-tag arguments.

func (*Parser) Remaining added in v0.4.0

func (p *Parser) Remaining() int

Remaining returns the number of unconsumed tokens.

func (*Parser) Set added in v0.6.0

func (p *Parser) Set() *Set

Set returns the template set associated with this parser, if any. Tag parsers can use this to load referenced templates at parse time (e.g., {% include %}, {% extends %}). Returns nil for parsers that were constructed via NewParser without an owning Set.

type PropertyAccessNode added in v0.4.0

type PropertyAccessNode struct {
	Object   Expression
	Property string
	Line     int
	Col      int
}

PropertyAccessNode represents property/attribute access.

func NewPropertyAccessNode added in v0.4.0

func NewPropertyAccessNode(object Expression, property string, line, col int) *PropertyAccessNode

NewPropertyAccessNode returns a new PropertyAccessNode.

func (*PropertyAccessNode) Evaluate added in v0.4.0

func (n *PropertyAccessNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate returns the property value from the evaluated object.

func (*PropertyAccessNode) Position added in v0.4.0

func (n *PropertyAccessNode) Position() (int, int)

Position returns the position of the PropertyAccessNode.

func (*PropertyAccessNode) String added in v0.4.0

func (n *PropertyAccessNode) String() string

String returns a debug representation of the PropertyAccessNode.

type Registry added in v0.4.0

type Registry struct {
	// contains filtered or unexported fields
}

Registry is a concurrency-safe collection of named filter functions. Use NewRegistry to create an instance, or use the package-level functions that operate on the default registry.

Registry supports an optional parent: when a lookup misses in this registry, the parent is consulted. This allows a Set to layer its own private filters (like safe and the HTML-aware escape) over the global registry without copying entries.

func NewRegistry added in v0.4.0

func NewRegistry() *Registry

NewRegistry creates an empty filter registry.

func (*Registry) Filter added in v0.4.0

func (r *Registry) Filter(name string) (FilterFunc, bool)

Filter returns the filter registered under name and a boolean indicating whether it was found. If not found locally and a parent registry is set, the parent is consulted.

func (*Registry) Has added in v0.4.0

func (r *Registry) Has(name string) bool

Has reports whether a filter with the given name is registered (including in ancestor registries).

func (*Registry) List added in v0.4.0

func (r *Registry) List() []string

List returns the names of all registered filters in sorted order.

func (*Registry) Register added in v0.4.0

func (r *Registry) Register(name string, fn FilterFunc)

Register adds a filter function under the given name. If a filter with the same name already exists, it is overwritten.

Register panics if fn is nil.

func (*Registry) Unregister added in v0.4.0

func (r *Registry) Unregister(name string)

Unregister removes the filter registered under name. It is a no-op if no such filter exists.

type SafeString added in v0.6.0

type SafeString string

SafeString marks a string value as pre-escaped or otherwise trusted HTML content. When a template rendered via NewHTMLSet outputs a SafeString, the auto-escaper bypasses escaping. In NewTextSet or Compile-path templates, SafeString behaves identically to a plain string.

Producing a SafeString from untrusted input is a security bug: the contents are emitted verbatim into HTML.

type Set added in v0.6.0

type Set struct {
	// contains filtered or unexported fields
}

Set is the entry point for the multi-file template system. A Set holds a Loader, a template cache, and rendering mode (HTML vs text).

Use NewHTMLSet for HTML output (auto-escapes {{ expr }}) or NewTextSet for non-HTML output (plain text, code generation, config files).

Sets are safe for concurrent use: compiled templates are cached and treated as read-only after compilation, so multiple goroutines can Render concurrently against the same Set.

func NewHTMLSet added in v0.6.0

func NewHTMLSet(loader Loader, opts ...SetOption) *Set

NewHTMLSet constructs a Set for HTML output. {{ expr }} is automatically HTML-escaped; values wrapped in SafeString (e.g. via the safe filter) are emitted verbatim. Use this for HTML pages, HTML emails, and any other HTML-context output.

func NewTextSet added in v0.6.0

func NewTextSet(loader Loader, opts ...SetOption) *Set

NewTextSet constructs a Set for plain-text output. {{ expr }} is rendered without any escaping. Use this for code generation, config files, YAML, TOML, Taskfile, plain-text emails, etc.

func (*Set) Get added in v0.6.0

func (s *Set) Get(name string) (*Template, error)

Get loads and compiles the named template, caching the result. Subsequent calls with the same name return the same *Template instance until Reset is called.

func (*Set) Render added in v0.6.0

func (s *Set) Render(name string, ctx Context, w io.Writer) error

Render loads the named template and writes its output to w. ctx is merged with any globals configured via WithGlobals; ctx keys take precedence over globals.

func (*Set) RenderString added in v0.6.0

func (s *Set) RenderString(name string, ctx Context) (string, error)

RenderString is a convenience wrapper around Render returning a string.

func (*Set) Reset added in v0.6.0

func (s *Set) Reset()

Reset clears the template cache. Intended for dev-server hot-reload workflows: call Reset after template files change on disk to force recompilation on the next Render.

type SetOption added in v0.6.0

type SetOption func(*Set)

SetOption configures a Set at construction time.

func WithGlobals added in v0.6.0

func WithGlobals(g Context) SetOption

WithGlobals injects variables available to every render call. Render-time ctx keys override globals on conflict.

type Statement added in v0.4.0

type Statement interface {
	Node
	Execute(ctx *ExecutionContext, writer io.Writer) error
}

Statement is the interface for all statement nodes. Statements are executed and produce output or side effects.

type SubscriptNode added in v0.4.0

type SubscriptNode struct {
	Object Expression
	Index  Expression
	Line   int
	Col    int
}

SubscriptNode represents subscript/index access.

func NewSubscriptNode added in v0.4.0

func NewSubscriptNode(object, index Expression, line, col int) *SubscriptNode

NewSubscriptNode returns a new SubscriptNode.

func (*SubscriptNode) Evaluate added in v0.4.0

func (n *SubscriptNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate returns the indexed or keyed value.

func (*SubscriptNode) Position added in v0.4.0

func (n *SubscriptNode) Position() (int, int)

Position returns the position of the SubscriptNode.

func (*SubscriptNode) String added in v0.4.0

func (n *SubscriptNode) String() string

String returns a debug representation of the SubscriptNode.

type TagParser added in v0.4.0

type TagParser func(doc *Parser, start *Token, arguments *Parser) (Statement, error)

TagParser is the parse function signature for template tags.

Parameters:

  • doc: The document-level parser used to parse nested tag bodies. Example: an if tag parses content between {% if %} and {% endif %}.
  • start: The tag-name token (for example "if", "for"), including source position information used in error reporting.
  • arguments: A dedicated parser for tag arguments. Example: in {% if x > 5 %}, this parser sees "x > 5".

Returns:

  • Statement: The parsed statement node.
  • error: Any parse error encountered.

func Tag added in v0.4.0

func Tag(name string) (TagParser, bool)

Tag returns the parser registered in the global registry for the given tag name. It is safe to call from multiple goroutines.

type TagRegistry added in v0.6.0

type TagRegistry struct {
	// contains filtered or unexported fields
}

TagRegistry stores tag parsers. An optional parent registry is consulted when a lookup misses, enabling a Set to layer its own private tags over the global registry without copying entries.

TagRegistry is safe for concurrent use.

func NewTagRegistry added in v0.6.0

func NewTagRegistry() *TagRegistry

NewTagRegistry creates an empty TagRegistry.

func (*TagRegistry) Get added in v0.6.0

func (r *TagRegistry) Get(name string) (TagParser, bool)

Get looks up a tag parser by name. If not found and a parent registry is set, the parent is consulted.

func (*TagRegistry) Has added in v0.6.0

func (r *TagRegistry) Has(name string) bool

Has reports whether a tag is registered (including in ancestors).

func (*TagRegistry) List added in v0.6.0

func (r *TagRegistry) List() []string

List returns a sorted list of tag names registered directly in this registry (excluding parents).

func (*TagRegistry) Register added in v0.6.0

func (r *TagRegistry) Register(name string, parser TagParser) error

Register adds a tag parser. Duplicate names return ErrTagAlreadyRegistered.

func (*TagRegistry) Unregister added in v0.6.0

func (r *TagRegistry) Unregister(name string)

Unregister removes a tag from this registry (does not touch parents).

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template represents a compiled template ready for execution. A Template is immutable after compilation.

Templates compiled via Compile(src) have nil set/name and cannot use include/extends. Templates loaded via Set.Get carry a set reference and a resolved name for caching, dependency tracking, and multi-file rendering.

func Compile added in v0.4.0

func Compile(source string) (*Template, error)

Compile compiles a template source string and returns an executable Template.

Compile performs lexical analysis, parsing, and template creation in sequence. On failure, the returned error wraps the underlying lexer or parser error.

tmpl, err := template.Compile("Hello {{ name }}!")
if err != nil {
    log.Fatal(err)
}
output, _ := tmpl.Render(map[string]any{"name": "World"})

func NewTemplate

func NewTemplate(root []Statement) *Template

NewTemplate creates a new Template from parsed AST nodes.

Most callers should use Compile instead, which handles lexing and parsing automatically.

func (*Template) Execute

func (t *Template) Execute(ctx *ExecutionContext, w io.Writer) error

Execute writes the template output to w using the given execution context.

For most use cases, Template.Render is simpler. Use Execute when you need control over the output destination or execution context.

When the template extends a parent (via {% extends %}), Execute walks up to the root of the extends chain and runs that template's body. The current template is recorded as ctx.currentLeaf so BlockNode.Execute can resolve overrides across the chain. For templates without a parent the loop is a no-op and the template runs its own body.

func (*Template) Render added in v0.4.0

func (t *Template) Render(data Context) (string, error)

Render executes the template with data and returns the output as a string.

Render is a convenience wrapper around Template.Execute for the common case where a string result is needed.

type TextNode added in v0.4.0

type TextNode struct {
	Text string
	Line int
	Col  int
}

TextNode represents plain text outside of template tags.

func NewTextNode added in v0.4.0

func NewTextNode(text string, line, col int) *TextNode

NewTextNode returns a new TextNode.

func (*TextNode) Execute added in v0.4.0

func (n *TextNode) Execute(_ *ExecutionContext, w io.Writer) error

Execute writes the raw text to the output.

func (*TextNode) Position added in v0.4.0

func (n *TextNode) Position() (int, int)

Position returns the position of the TextNode.

func (*TextNode) String added in v0.4.0

func (n *TextNode) String() string

String returns a debug representation of the TextNode.

type Token added in v0.2.0

type Token struct {
	Type  TokenType
	Value string
	Line  int // 1-based
	Col   int // 1-based
}

Token represents a single lexical token.

func (*Token) String added in v0.4.0

func (t *Token) String() string

String returns a human-readable representation of the token.

type TokenType added in v0.2.0

type TokenType int

TokenType represents the type of a token.

const (
	// TokenError indicates a lexical error.
	TokenError TokenType = iota

	// TokenEOF indicates the end of input.
	TokenEOF

	// TokenText represents plain text outside of template tags.
	TokenText

	// TokenVarBegin represents the {{ variable tag opener.
	TokenVarBegin

	// TokenVarEnd represents the }} variable tag closer.
	TokenVarEnd

	// TokenTagBegin represents the {% block tag opener.
	TokenTagBegin

	// TokenTagEnd represents the %} block tag closer.
	TokenTagEnd

	// TokenIdentifier represents an identifier such as a variable name or keyword.
	TokenIdentifier

	// TokenString represents a quoted string literal.
	TokenString

	// TokenNumber represents an integer or floating-point literal.
	TokenNumber

	// TokenSymbol represents an operator or punctuation character.
	TokenSymbol
)

func (TokenType) String added in v0.4.0

func (t TokenType) String() string

String returns a string representation of the token type.

type UnaryOpNode added in v0.4.0

type UnaryOpNode struct {
	Operator string
	Operand  Expression
	Line     int
	Col      int
}

UnaryOpNode represents a unary operation.

func NewUnaryOpNode added in v0.4.0

func NewUnaryOpNode(operator string, operand Expression, line, col int) *UnaryOpNode

NewUnaryOpNode returns a new UnaryOpNode.

func (*UnaryOpNode) Evaluate added in v0.4.0

func (n *UnaryOpNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate computes the unary operation result.

func (*UnaryOpNode) Position added in v0.4.0

func (n *UnaryOpNode) Position() (int, int)

Position returns the position of the UnaryOpNode.

func (*UnaryOpNode) String added in v0.4.0

func (n *UnaryOpNode) String() string

String returns a debug representation of the UnaryOpNode.

type Value added in v0.2.0

type Value struct {
	// contains filtered or unexported fields
}

Value wraps a Go value for template execution, providing type checking, conversion, and comparison operations.

func NewValue added in v0.2.0

func NewValue(v any) *Value

NewValue creates a Value wrapping v.

func (*Value) Bool added in v0.2.0

func (v *Value) Bool() bool

Bool reports whether the value is truthy.

func (*Value) Compare added in v0.4.0

func (v *Value) Compare(other *Value) (int, error)

Compare compares v with other. It returns -1 if v < other, 0 if v == other, 1 if v > other.

func (*Value) Equals added in v0.4.0

func (v *Value) Equals(other *Value) bool

Equals reports whether v and other represent the same value.

func (*Value) Field added in v0.4.0

func (v *Value) Field(name string) (*Value, error)

Field returns the value of a struct field or map key by name. For structs, it searches by JSON tag first, then by exported field name.

func (*Value) Float added in v0.2.0

func (v *Value) Float() (float64, error)

Float returns the value as float64, converting if possible.

func (*Value) Index added in v0.4.0

func (v *Value) Index(i int) (*Value, error)

Index returns the element at index i (for slices, arrays, strings).

func (*Value) Int added in v0.2.0

func (v *Value) Int() (int64, error)

Int returns the value as int64, converting if possible.

func (*Value) Interface added in v0.4.0

func (v *Value) Interface() any

Interface returns the underlying Go value.

func (*Value) IsNil added in v0.4.0

func (v *Value) IsNil() bool

IsNil reports whether the value is nil.

func (*Value) IsTrue added in v0.4.0

func (v *Value) IsTrue() bool

IsTrue reports whether the value is truthy in a template context. False values: nil, false, 0, "", empty slice/map/array.

func (*Value) Iterate added in v0.4.0

func (v *Value) Iterate(fn func(idx, count int, key, val *Value) bool) error

Iterate calls fn for each element in a collection (slice, array, map, string). fn receives the iteration index, total count, key, and value. Returning false from fn stops iteration early.

func (*Value) Key added in v0.4.0

func (v *Value) Key(key any) (*Value, error)

Key returns the map value for the given key.

func (*Value) Len added in v0.4.0

func (v *Value) Len() (int, error)

Len returns the length of the value (string, slice, map, or array).

func (*Value) String added in v0.4.0

func (v *Value) String() string

String returns the string representation of the value.

type VariableNode added in v0.2.0

type VariableNode struct {
	Name string
	Line int
	Col  int
}

VariableNode represents a variable reference.

func NewVariableNode added in v0.4.0

func NewVariableNode(name string, line, col int) *VariableNode

NewVariableNode returns a new VariableNode.

func (*VariableNode) Evaluate added in v0.2.0

func (n *VariableNode) Evaluate(ctx *ExecutionContext) (*Value, error)

Evaluate resolves the variable in the current execution context.

func (*VariableNode) Position added in v0.4.0

func (n *VariableNode) Position() (int, int)

Position returns the position of the VariableNode.

func (*VariableNode) String added in v0.4.0

func (n *VariableNode) String() string

String returns a debug representation of the VariableNode.

Directories

Path Synopsis
examples
custom_filters command
Package main demonstrates registering custom filters.
Package main demonstrates registering custom filters.
custom_tags command
Package main demonstrates registering a custom tag via RegisterTag.
Package main demonstrates registering a custom tag via RegisterTag.
layout command
Package main demonstrates multi-file HTML templating with layout inheritance, includes, block.super, and HTML auto-escape.
Package main demonstrates multi-file HTML templating with layout inheritance, includes, block.super, and HTML auto-escape.
multifile_text command
Package main demonstrates multi-file text generation with NewTextSet.
Package main demonstrates multi-file text generation with NewTextSet.
usage command
Package main demonstrates typical template usage.
Package main demonstrates typical template usage.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL