jinja

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2025 License: MIT Imports: 11 Imported by: 2

README

Jinja Go

A Go library that mimics Jinja's templating behavior.

Project Goals

This project aims to provide a reusable library with two main functions:

  • TemplateString(template string, context map[string]interface{}) (string, error): Evaluates a Jinja-like template string. Variables in the format {{ variable_name }} are replaced with values from the context map.
  • EvaluateExpression(expression string, context map[string]interface{}) (interface{}, error): Evaluates a Jinja-like expression string using the provided context. An error is returned if the expression cannot be evaluated.

Additionally, the library will support:

  • Built-in functions and filters comparable to those in Jinja (e.g., lookup, urlencode, map, default).
  • Basic flow control structures (e.g., {% for item in items %}, {% if condition %}).

Usage

package main

import (
	"fmt"

	"github.com/AlexanderGrooff/jinja-go"
)

func main() {
	context := map[string]interface{}{
		"name": "World",
		"isAdmin": true,
	}

	// TemplateString example
	templated, err := jinja.TemplateString("Hello {{ name }}!", context)
	if err != nil {
		fmt.Printf("TemplateString Error: %v\n", err)
		return
	}
	fmt.Println(templated) // Output: Hello World!

	// EvaluateExpression example
	isAdmin, err := jinja.EvaluateExpression("isAdmin", context)
	if err != nil {
		fmt.Printf("EvaluateExpression Error: %v\n", err)
		return
	}
	fmt.Printf("Is Admin: %v\n", isAdmin) // Output: Is Admin: true
} 

Benchmarking

Performance is critical for this library. We use benchmarking to ensure that changes don't negatively impact performance.

Running Benchmarks
# Run benchmarks without saving results
make benchmark

# Run benchmarks and save as latest
make benchmark-save

# Compare latest benchmarks with previous
make benchmark-compare

# Save latest as the new previous (baseline)
make benchmark-save-as-previous

# Compare with another branch
make benchmark-branch branch=main

# Generate and save a benchmark report
make benchmark-report

# Run cross-language benchmarks against Python's Jinja2
make cross-benchmark

The repository uses benchstat to compare benchmark results, and pre-commit hooks automatically run benchmarks and compare with previous results.

Profiling

In addition to benchmarking, the library includes profiling tools to identify performance bottlenecks and optimize critical sections of code.

Quick Start
# Profile the complex_template with CPU, memory, and block profiling
make profile-complex

# Profile nested_loops template (one of the most performance-critical patterns)
make profile-nested-loops

# Profile all templates from the benchmark suite
make profile-all

# Run custom profiling
make profile ARGS="--template conditional --cpu --iterations 5000"
Analyzing Profiles

After running a profile, analyze the results:

# Web-based visualization (most comprehensive)
go tool pprof -http=:8080 profile_results/complex_template/cpu.prof

# Text-based analysis
go tool pprof profile_results/template_name/cpu.prof
(pprof) top10                # Show top 10 functions by CPU usage
(pprof) list TemplateString  # Show time spent in function

For more detailed information on profiling and performance optimization guidelines, see performance.md.

Cross-Language Benchmarks

You can directly compare this Go implementation against Python's Jinja2 and other Go-based Jinja-like libraries (such as Pongo2) using the cross-language benchmarking tools:

# Run with default settings
make cross-benchmark

# Run with custom iterations and output directory
./cmd/benchmark/run_benchmarks.sh --iterations 5000 --output-dir custom_benchmarks

# Run with custom template test cases
./cmd/benchmark/run_benchmarks.sh --templates path/to/custom_templates.json

The cross-benchmark tool:

  1. Runs identical templates through both the Python and Go implementations (including other Go libraries like Pongo2)
  2. Measures rendering time for each template
  3. Calculates the speed difference between implementations
  4. Generates a detailed comparison report

Custom template test cases can be defined in a JSON file following this format:

[
  {
    "name": "template_name",
    "template": "Hello, {{ name }}!",
    "context": {"name": "World"}
  },
  // More test cases...
]

The comparison provides insight into performance characteristics of both implementations, which is useful for:

  • Identifying areas where the Go implementation can be optimized
  • Quantifying performance gains for various template features
  • Tracking performance improvements over time

You can view the latest comparison report to see the current performance differences between all implementations.

Implementation Status

Already Implemented Features
  • Template Syntax

    • Basic variable substitution ({{ variable }})
    • Comments ({# comment #})
    • Conditional statements ({% if %}, {% elif %}, {% else %}, {% endif %})
    • Loop structures ({% for item in items %}, {% endfor %}) with loop variable support
  • Expression Evaluation

    • Basic literals (integers, floats, strings, booleans, null/None)
    • Variable access and context lookup
    • Pythonic data types:
      • Lists ([1, 2, 3])
      • Dictionaries ({'key': 'value'})
      • Dictionary methods like .get() (dict.get('key', 'default'))
      • String methods like .format() ("Hello, {}!".format("world"))
    • Object/attribute access (object.attribute)
    • Subscript access (array[index], dict['key'], negative indices)
    • LALR (Look-Ahead LR) parser for robust expression evaluation
      • Improved parsing performance and reliability
      • Proper operator precedence handling
      • Support for complex expressions such as a * (b + c) / d
    • Complex nested expression handling with multiple subscript operations
    • Basic filters (e.g., {{ var | default('fallback') }})
  • Operators

    • Arithmetic operators (+, -, *, /, // (floor division), % (modulo), ** (power))
    • Unary operators (not, -, +)
    • Comparison operators (==, !=, >, <, >=, <=)
    • Logical operators (and, or) with short-circuit evaluation
    • Identity operators (is, is not)
    • Membership operators (in)
    • String operations (concatenation, repetition)
  • Filters

    • default filter
    • join filter
    • upper filter
    • lower filter
    • capitalize filter
    • replace filter
    • trim filter
    • list filter
    • escape filter
    • map filter
    • items filter
    • lookup filter with file and env sources
Planned Features
  • Template Syntax

    • Include support ({% include 'page.html' %})
    • Macro definitions ({% macro %}/{% endmacro %})
    • Block and extends for template inheritance ({% block %}, {% extends %})
    • Set statements ({% set %})
    • With blocks ({% with %})
    • Loop controls ({% break %}, {% continue %})
    • Whitespace control (using - in tags like {%- and -%})
    • Expression statements ({% do expression %})
    • Debug statements ({% debug %})
  • Expression Evaluation

    • More filters (e.g., {{ url | urlencode }})
    • Tests ({{ user is defined }}, {{ user is not none }})
    • String formatting and f-strings
    • List comprehensions
    • Generator expressions (iterables)
    • Auto-escaping support
  • Control Structures

    • More complex control structures
  • Filters and Functions

    • Additional common Jinja filters (urlencode, etc.)
    • Additional lookup plugin types for the lookup filter
    • More built-in functions
    • Complete set of built-in tests (defined, none, iterable, etc.)
    • Translation/internationalization support (gettext)
  • Advanced Features

    • Macro definitions
    • Include/import functionality
    • Block/extends for template inheritance
    • Context scoping and namespaces
    • Custom tests and filters
    • Auto-escaping configuration
Broader improvements to be made
  1. Error handling improvements - standardize error reporting across modules
  2. Handling of edge cases in string literals and escaping
  3. Better documentation of supported features

License

This project is licensed under the MIT License - see the LICENSE file for details.

Pre-commit Hooks

The pre-commit hooks will:

  1. Run benchmarks before each commit
  2. Compare with previous benchmark results
  3. Show performance changes

Install pre-commit hooks with:

pre-commit install

Documentation

Index

Constants

View Source
const (
	PrecedenceOr      = 10
	PrecedenceAnd     = 20
	PrecedenceNot     = 30
	PrecedenceCompare = 40
	PrecedenceAdd     = 50
	PrecedenceMul     = 60
	PrecedencePow     = 70
)

Operator precedence levels (higher means higher precedence)

Variables

View Source
var GlobalFilters map[string]FilterFunc

GlobalFilters stores the registered filter functions.

View Source
var GlobalFunctions map[string]FunctionFunc

GlobalFunctions stores the registered functions that can be called directly in templates

View Source
var GlobalMethods map[string]map[string]FunctionFunc

GlobalMethods stores methods that can be called on objects of specific types

View Source
var GlobalTests map[string]TestFunc

GlobalTests stores the registered Jinja test functions.

View Source
var Omit = OmitType{}

Omit is the singleton value representing the 'omit' keyword

View Source
var Undefined = UndefinedType{}

Undefined is the singleton value representing an undefined variable

Functions

func EvaluateExpression

func EvaluateExpression(expression string, context map[string]interface{}) (interface{}, error)

EvaluateExpression evaluates a single expression string (without surrounding {{ }}) against the provided context. It applies filters as specified. If the variable is undefined after evaluation (and not handled by a filter like default), an error is returned.

func IsTruthy added in v0.1.2

func IsTruthy(value interface{}) bool

IsTruthy determines if a value is considered "truthy" in the Python/Jinja2 sense

func ParseAndEvaluate

func ParseAndEvaluate(expr string, context map[string]interface{}) (interface{}, error)

ParseAndEvaluate parses and evaluates an expression string with context

func ParseVariables added in v0.1.2

func ParseVariables(template string) ([]string, error)

ParseVariables extracts all Jinja variable names from a template string. It returns a slice of unique variable names found in expressions {{ ... }} and control tags {% ... %}. For example, "some string with a {{ item.name | default('name') }}" returns ["item"].

func ParseVariablesFromExpression added in v0.1.7

func ParseVariablesFromExpression(expression string) ([]string, error)

ParseVariablesFromExpression extracts all root Jinja variable names from a Jinja expression string. For example, for the expression `item.some_key`, it returns ["item"]. For `item.some_bool and another_item`, it returns ["item", "another_item"].

func TemplateString

func TemplateString(template string, context map[string]interface{}) (string, error)

TemplateString renders a template string using the provided context. It processes Jinja-like expressions {{ ... }}, comments {# ... #}, and control tags {% ... %}.

func TemplateStringInContext added in v0.3.0

func TemplateStringInContext(template string, context map[string]interface{}, searchDirs []string) (string, error)

TemplateStringInContext renders a template with custom directories used for file resolution. When searchDirs is non-empty, relative paths are resolved by checking each directory in order.

func TryEvaluateSingleExpressionTemplate added in v0.3.0

func TryEvaluateSingleExpressionTemplate(template string, context map[string]interface{}) (interface{}, bool, bool, error)

TryEvaluateSingleExpressionTemplate determines if the provided template consists of exactly one expression node (ignoring surrounding whitespace and comments), and if so, evaluates it and returns the typed value. It returns: - value: the evaluated value of the single expression - isSingle: whether the template is a single expression template - wasUndefined: whether the expression evaluated to a strictly undefined value - err: any parsing or evaluation error encountered

Types

type CompareOp added in v0.1.3

type CompareOp int

CompareOp represents a comparison operation

const (
	OpLT CompareOp = iota // <
	OpLE                  // <=
	OpGT                  // >
	OpGE                  // >=
	OpEQ                  // ==
	OpNE                  // !=
)

type ControlTagInfo

type ControlTagInfo struct {
	Type       ControlTagType
	Expression string // For 'if', 'elif', 'for': the condition or loop expression.

}

ControlTagInfo holds detailed information about a parsed control tag.

type ControlTagType

type ControlTagType string

ControlTagType defines the specific type of a control tag.

const (
	ControlIf      ControlTagType = "if"
	ControlEndIf   ControlTagType = "endif"
	ControlFor     ControlTagType = "for"    // Placeholder for future 'for' loop implementation
	ControlEndFor  ControlTagType = "endfor" // Placeholder for future 'endfor' implementation
	ControlElse    ControlTagType = "else"   // Placeholder for future 'else' implementation
	ControlElseIf  ControlTagType = "elif"   // Placeholder for future 'elif' (else if) implementation
	ControlInclude ControlTagType = "include"
	ControlUnknown ControlTagType = "unknown"
)

Enumerates the different types of control tags.

type EvaluateExpressionFunc

type EvaluateExpressionFunc func(expression string, context map[string]interface{}) (interface{}, error)

EvaluateExpressionFunc defines the signature for an expression evaluation function. This is used to pass EvaluateExpression logic to statement handlers.

type Evaluator

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

Evaluator evaluates an AST for a Jinja expression using a provided context.

func NewEvaluator

func NewEvaluator(context map[string]interface{}) *Evaluator

NewEvaluator creates a new evaluator with a merged context (user context + global functions).

func (*Evaluator) Evaluate

func (e *Evaluator) Evaluate(node *ExprNode) (interface{}, error)

Evaluate recursively evaluates an AST node using the evaluator's context.

type ExprNode

type ExprNode struct {
	Type       ExprNodeType
	Value      interface{}
	Children   []*ExprNode
	Operator   string
	Identifier string
	FilterName string
	FilterArgs []*ExprNode
}

ExprNode represents a node in the expression AST

type ExprNodeType

type ExprNodeType int

ExprNodeType represents the type of AST node

const (
	NodeLiteral ExprNodeType = iota
	NodeIdentifier
	NodeUnaryOp
	NodeBinaryOp
	NodeAttribute
	NodeSubscript
	NodeFunctionCall
	NodeList
	NodeDict
	NodeTuple
	NodeFilterChain
)

type ExprParser

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

ExprParser parses a sequence of tokens into an abstract syntax tree (AST) for Jinja expressions.

func NewExprParser

func NewExprParser(tokens []Token) *ExprParser

NewExprParser creates a new parser for the given tokens.

func (*ExprParser) Parse

func (p *ExprParser) Parse() (*ExprNode, error)

Parse parses the tokens into an AST root node.

type FileResolver added in v0.3.0

type FileResolver func(path string) ([]byte, error)

FileResolver resolves file contents for include/lookup with custom search rules

type FilterFunc

type FilterFunc func(input interface{}, args ...interface{}) (interface{}, error)

FilterFunc defines the signature for a filter function. input is the value to be filtered. args are the arguments passed to the filter.

type FunctionFunc

type FunctionFunc func(e *Evaluator, args ...interface{}) (interface{}, error)

FunctionFunc defines the signature for a function callable from templates

type Lexer

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

Lexer is responsible for breaking an input string into tokens for parsing Jinja expressions.

func NewLexer

func NewLexer(input string) *Lexer

NewLexer creates a new lexer instance for the given input string.

func (*Lexer) Tokenize

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

Tokenize breaks the input string into a slice of tokens.

type Node

type Node struct {
	Type    NodeType        // The kind of node (e.g., text, expression).
	Content string          // The raw content of the node.
	Control *ControlTagInfo // Populated if Type is NodeControlTag, provides details about the control tag.

}

Node represents a parsed element in the template, such as literal text or an expression.

type NodeType

type NodeType int

NodeType defines the category of a parsed Node.

const (
	NodeText       NodeType = iota // Represents a segment of literal text.
	NodeExpression                 // Represents a Jinja expression, e.g., {{ variable }}.
	NodeComment                    // Represents a comment, e.g., {# comment #}.
	NodeControlTag                 // Represents a control structure tag, e.g., {% if ... %}.
)

Enumerates the different types of nodes that can be encountered during parsing.

type OmitType added in v0.3.0

type OmitType struct{}

OmitType is a sentinel type for the 'omit' keyword in Jinja expressions It is used in filters like default(omit) to indicate that a value should be omitted

type Parser

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

Parser holds the state of the parsing process. It is used to iteratively parse a template string into a sequence of nodes.

func NewParser

func NewParser(input string) *Parser

NewParser creates a new Parser instance for the given input string.

func (*Parser) ParseAll

func (p *Parser) ParseAll() ([]*Node, error)

ParseAll parses the entire template into a slice of nodes.

func (*Parser) ParseNext

func (p *Parser) ParseNext() (*Node, error)

ParseNext returns the next node (text or expression) from the input stream. It returns (nil, nil) when EOF is reached. It relies on p.parseExpressionTag to handle the intricacies of expression parsing, including resetting p.pos if an expression tag is not properly closed.

type ProcessNodesFunc

type ProcessNodesFunc func(nodes []*Node, context map[string]interface{}) (string, error)

ProcessNodesFunc defines the signature for the node processing function. This is used to allow statement handlers to recursively process blocks of nodes.

type TemplateCache

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

TemplateCache is a thread-safe cache for parsed templates

func NewTemplateCache

func NewTemplateCache() *TemplateCache

NewTemplateCache creates a new template cache

func (*TemplateCache) Get

func (tc *TemplateCache) Get(template string) ([]*Node, bool)

Get retrieves parsed nodes for a template from the cache

func (*TemplateCache) Set

func (tc *TemplateCache) Set(template string, nodes []*Node)

Set stores parsed nodes for a template in the cache

type TestFunc added in v0.1.8

type TestFunc func(input interface{}, args ...interface{}) (bool, error)

TestFunc defines the signature for a Jinja test function.

type Token

type Token struct {
	Type     TokenType
	Value    string
	Position int
}

Token represents a lexical token in a Jinja expression

type TokenType

type TokenType int

TokenType represents different types of tokens in a Jinja expression

const (
	TokenLiteral TokenType = iota
	TokenIdentifier
	TokenOperator
	TokenLeftParen
	TokenRightParen
	TokenLeftBracket
	TokenRightBracket
	TokenLeftBrace
	TokenRightBrace
	TokenComma
	TokenDot
	TokenColon
	TokenPipe
	TokenEOF
)

type UndefinedType added in v0.1.8

type UndefinedType struct{}

UndefinedType is a sentinel type for undefined variables in Jinja expressions It is used to distinguish between nil (which is a valid value) and undefined (which is not set in context)

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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