pongo2 allows you to extend the template engine with custom filters and tags.
Filters transform variable values. They are functions with the signature:
type FilterFunction func(in *Value, param *Value) (out *Value, err error)func init() {
pongo2.RegisterFilter("double", filterDouble)
}
func filterDouble(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, error) {
return pongo2.AsValue(in.Integer() * 2), nil
}Usage in template:
{{ 21|double }} {# Output: 42 #}func init() {
pongo2.RegisterFilter("multiply", filterMultiply)
}
func filterMultiply(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, error) {
return pongo2.AsValue(in.Integer() * param.Integer()), nil
}Usage:
{{ 7|multiply:6 }} {# Output: 42 #}func filterDivide(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, error) {
divisor := param.Integer()
if divisor == 0 {
return nil, &pongo2.Error{
Sender: "filter:divide",
OrigError: errors.New("division by zero"),
}
}
return pongo2.AsValue(in.Integer() / divisor), nil
}The Value type wraps Go values and provides helper methods:
func myFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, error) {
// Type checking
if in.IsString() { /* ... */ }
if in.IsInteger() { /* ... */ }
if in.IsFloat() { /* ... */ }
if in.IsBool() { /* ... */ }
if in.IsNil() { /* ... */ }
if in.IsNumber() { /* ... */ } // Integer or Float
// Type conversion
s := in.String() // Convert to string
i := in.Integer() // Convert to int
f := in.Float() // Convert to float64
b := in.Bool() // Convert to bool
// Collection operations
if in.CanSlice() {
length := in.Len()
first := in.Index(0)
slice := in.Slice(0, 5)
}
// Check truthiness (for conditionals)
if in.IsTrue() { /* ... */ }
// Get underlying interface{}
raw := in.Interface()
// Return values
return pongo2.AsValue("result"), nil // Regular value
return pongo2.AsSafeValue("<b>html</b>"), nil // Safe (no escaping)
}func init() {
// Override the built-in upper filter
pongo2.ReplaceFilter("upper", myUpperFilter)
}if pongo2.BuiltinFilterExists("myfilter") {
// Filter is registered
}value := pongo2.AsValue("hello")
param := pongo2.AsValue(nil)
result, err := pongo2.ApplyFilter("upper", value, param)
// result.String() == "HELLO"
// Panic version
result = pongo2.MustApplyFilter("upper", value, param)Tags are more complex than filters. They can:
- Access and modify the parser
- Wrap content (block tags)
- Execute logic during rendering
type TagParser func(doc *Parser, start *Token, arguments *Parser) (INodeTag, error)Parameters:
doc- The document parser (for parsing nested content)start- The token containing the tag namearguments- Parser for the tag's arguments
A tag that outputs the current time:
func init() {
pongo2.RegisterTag("current_time", tagCurrentTimeParser)
}
type tagCurrentTimeNode struct {
format string
}
func (node *tagCurrentTimeNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
writer.WriteString(time.Now().Format(node.format))
return nil
}
func tagCurrentTimeParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, error) {
node := &tagCurrentTimeNode{
format: time.RFC3339, // default format
}
// Parse optional format argument
if formatToken := arguments.MatchType(pongo2.TokenString); formatToken != nil {
node.format = formatToken.Val
}
// Check for extra arguments
if arguments.Remaining() > 0 {
return nil, arguments.Error("Malformed current_time tag", nil)
}
return node, nil
}Usage:
{% current_time %}
{% current_time "2006-01-02" %}A tag that wraps content:
func init() {
pongo2.RegisterTag("uppercase", tagUppercaseParser)
}
type tagUppercaseNode struct {
wrapper *pongo2.NodeWrapper
}
func (node *tagUppercaseNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
// Capture the block content
var buf bytes.Buffer
err := node.wrapper.Execute(ctx, &buf)
if err != nil {
return err
}
// Transform and output
writer.WriteString(strings.ToUpper(buf.String()))
return nil
}
func tagUppercaseParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, error) {
node := &tagUppercaseNode{}
// Parse until enduppercase
wrapper, endargs, err := doc.WrapUntilTag("enduppercase")
if err != nil {
return nil, err
}
node.wrapper = wrapper
// enduppercase shouldn't have arguments
if endargs.Count() > 0 {
return nil, endargs.Error("enduppercase takes no arguments", nil)
}
return node, nil
}Usage:
{% uppercase %}
hello world
{% enduppercase %}
{# Output: HELLO WORLD #}Parse expressions that can contain variables:
type tagRepeatNode struct {
countExpr pongo2.IEvaluator
wrapper *pongo2.NodeWrapper
}
func (node *tagRepeatNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
// Evaluate the count expression
countVal, err := node.countExpr.Evaluate(ctx)
if err != nil {
return err
}
count := countVal.Integer()
for i := 0; i < count; i++ {
err := node.wrapper.Execute(ctx, writer)
if err != nil {
return err
}
}
return nil
}
func tagRepeatParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, error) {
node := &tagRepeatNode{}
// Parse the count expression
countExpr, err := arguments.ParseExpression()
if err != nil {
return nil, err
}
node.countExpr = countExpr
// Parse until endrepeat
wrapper, _, err := doc.WrapUntilTag("endrepeat")
if err != nil {
return nil, err
}
node.wrapper = wrapper
return node, nil
}Usage:
{% repeat 3 %}Hello {% endrepeat %}
{# Output: Hello Hello Hello #}
{% repeat count %}Item {% endrepeat %}Handle tags like if/elif/else/endif:
wrapper, endargs, err := doc.WrapUntilTag("elif", "else", "endif")
if err != nil {
return nil, err
}
switch wrapper.Endtag {
case "elif":
// Parse elif condition and continue
case "else":
// Parse else block
case "endif":
// Done
}Common methods for parsing arguments:
// Match specific token type and value
if token := arguments.Match(pongo2.TokenKeyword, "as"); token != nil {
// Matched "as" keyword
}
// Match any of several values
if token := arguments.MatchOne(pongo2.TokenIdentifier, "asc", "desc"); token != nil {
// Matched either "asc" or "desc"
}
// Match by type only
if token := arguments.MatchType(pongo2.TokenString); token != nil {
value := token.Val
}
if token := arguments.MatchType(pongo2.TokenNumber); token != nil {
// ...
}
if token := arguments.MatchType(pongo2.TokenIdentifier); token != nil {
name := token.Val
}
// Parse a full expression
expr, err := arguments.ParseExpression()
// Check remaining arguments
if arguments.Remaining() > 0 {
return nil, arguments.Error("Too many arguments", nil)
}
// Peek without consuming
if arguments.Peek(pongo2.TokenSymbol, "=") != nil {
// Next token is "="
}Access template context during execution:
func (node *myNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
// Read from public context (user-provided)
user := ctx.Public["user"]
// Read/write private context (internal use)
ctx.Private["my_counter"] = 0
// Check autoescape setting
if ctx.Autoescape {
// HTML escaping is enabled
}
// Log debug messages (only when Debug=true)
ctx.Logf("Processing item %d", itemNum)
// Create error with template location
return ctx.Error("Something went wrong", node.token)
}For tags that create new variable scopes:
func (node *myNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
// Create child context (inherits parent's variables)
childCtx := pongo2.NewChildExecutionContext(ctx)
// Add scoped variables
childCtx.Private["loop_var"] = someValue
// Execute wrapped content with child context
return node.wrapper.Execute(childCtx, writer)
}func init() {
pongo2.ReplaceTag("for", myCustomForParser)
}A tag that caches rendered content:
package main
import (
"bytes"
"sync"
"time"
"github.com/flosch/pongo2/v7"
)
var (
cache = make(map[string]cacheEntry)
cacheMutex sync.RWMutex
)
type cacheEntry struct {
content string
expiresAt time.Time
}
type tagCacheNode struct {
key string
duration time.Duration
wrapper *pongo2.NodeWrapper
}
func (node *tagCacheNode) Execute(ctx *pongo2.ExecutionContext, writer pongo2.TemplateWriter) error {
// Check cache
cacheMutex.RLock()
entry, exists := cache[node.key]
cacheMutex.RUnlock()
if exists && time.Now().Before(entry.expiresAt) {
writer.WriteString(entry.content)
return nil
}
// Render content
var buf bytes.Buffer
err := node.wrapper.Execute(ctx, &buf)
if err != nil {
return err
}
content := buf.String()
// Store in cache
cacheMutex.Lock()
cache[node.key] = cacheEntry{
content: content,
expiresAt: time.Now().Add(node.duration),
}
cacheMutex.Unlock()
writer.WriteString(content)
return nil
}
func tagCacheParser(doc *pongo2.Parser, start *pongo2.Token, arguments *pongo2.Parser) (pongo2.INodeTag, error) {
node := &tagCacheNode{
duration: 5 * time.Minute, // default
}
// Parse cache key
keyToken := arguments.MatchType(pongo2.TokenString)
if keyToken == nil {
return nil, arguments.Error("cache tag requires a key string", nil)
}
node.key = keyToken.Val
// Parse optional duration
if durationToken := arguments.MatchType(pongo2.TokenNumber); durationToken != nil {
seconds := pongo2.AsValue(durationToken.Val).Integer()
node.duration = time.Duration(seconds) * time.Second
}
// Parse content
wrapper, _, err := doc.WrapUntilTag("endcache")
if err != nil {
return nil, err
}
node.wrapper = wrapper
return node, nil
}
func init() {
pongo2.RegisterTag("cache", tagCacheParser)
}Usage:
{% cache "sidebar" 300 %}
<div class="sidebar">
{{ expensive_query() }}
</div>
{% endcache %}