go-twig/twig.go
semihalev c663834595 Fix debug tests and improve debug functionality
1. Fixed and enabled previously skipped debug tests:
   - TestDebugConditionals
   - TestDebugErrorReporting
2. Added detailed logging for conditional evaluation
3. Enhanced error reporting for undefined variables and expressions
4. Added template name tracking for better error context
5. Fixed formatting issues in log messages
6. Updated debug flag handling across the engine
2025-03-11 12:48:58 +03:00

652 lines
17 KiB
Go

package twig
import (
"bytes"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
)
// Engine represents the Twig template engine
type Engine struct {
templates map[string]*Template
mu sync.RWMutex
autoReload bool
strictVars bool
loaders []Loader
environment *Environment
debug bool
currentTemplate string // Tracks the name of the template currently being rendered
// Test helper - override Parse function
Parse func(source string) (*Template, error)
}
// Template represents a parsed and compiled Twig template
type Template struct {
name string
source string
nodes Node
env *Environment
engine *Engine // Reference back to the engine for loading parent templates
loader Loader // The loader that loaded this template
lastModified int64 // Last modified timestamp for this template
}
// Environment holds configuration and context for template rendering
type Environment struct {
globals map[string]interface{}
filters map[string]FilterFunc
functions map[string]FunctionFunc
tests map[string]TestFunc
operators map[string]OperatorFunc
extensions []Extension
cache bool
autoescape bool
debug bool
sandbox bool
}
// New creates a new Twig engine instance
func New() *Engine {
env := &Environment{
globals: make(map[string]interface{}),
filters: make(map[string]FilterFunc),
functions: make(map[string]FunctionFunc),
tests: make(map[string]TestFunc),
operators: make(map[string]OperatorFunc),
autoescape: true,
cache: true, // Enable caching by default
debug: false, // Disable debug mode by default
}
engine := &Engine{
templates: make(map[string]*Template),
environment: env,
autoReload: false, // Disable auto-reload by default
}
// Register the core extension by default
engine.AddExtension(&CoreExtension{})
return engine
}
// RegisterLoader adds a template loader to the engine
func (e *Engine) RegisterLoader(loader Loader) {
e.loaders = append(e.loaders, loader)
}
// SetAutoReload sets whether templates should be reloaded on change
func (e *Engine) SetAutoReload(autoReload bool) {
e.autoReload = autoReload
}
// SetStrictVars sets whether strict variable access is enabled
func (e *Engine) SetStrictVars(strictVars bool) {
e.strictVars = strictVars
}
// SetDebug enables or disables debug mode
func (e *Engine) SetDebug(enabled bool) {
e.environment.debug = enabled
e.debug = enabled
// Set the debug level based on the debug flag
if enabled {
SetDebugLevel(DebugInfo)
} else {
SetDebugLevel(DebugOff)
}
}
// SetCache enables or disables template caching
func (e *Engine) SetCache(enabled bool) {
e.environment.cache = enabled
}
// SetDevelopmentMode enables settings appropriate for development
// This sets debug mode on, enables auto-reload, and disables caching
func (e *Engine) SetDevelopmentMode(enabled bool) {
e.environment.debug = enabled
e.debug = enabled
e.autoReload = enabled
e.environment.cache = !enabled
}
// Render renders a template with the given context
func (e *Engine) Render(name string, context map[string]interface{}) (string, error) {
LogInfo("Rendering template: %s", name)
// Store current template name and previous template name
prevTemplate := e.currentTemplate
e.currentTemplate = name
defer func() {
// Restore previous template name when we're done
e.currentTemplate = prevTemplate
}()
template, err := e.Load(name)
if err != nil {
LogError(err, fmt.Sprintf("Failed to load template: %s", name))
return "", err
}
// If debug is enabled, use more detailed error reporting
if e.environment.debug {
var buf StringBuffer
ctx := NewRenderContext(e.environment, context, e)
defer ctx.Release()
// Use debug rendering with enhanced error reporting
err = DebugRender(&buf, template, ctx)
if err != nil {
LogError(err, fmt.Sprintf("Error rendering template: %s", name))
// Enhance error with template information
if enhancedErr, ok := err.(*EnhancedError); !ok {
err = NewError(err, name, 0, 0, template.source)
} else {
// Already enhanced, just ensure template name is set
if enhancedErr.Template == "" {
enhancedErr.Template = name
}
}
return "", err
}
return buf.String(), nil
}
// Normal rendering path without debug overhead
return template.Render(context)
}
// RenderTo renders a template to a writer
func (e *Engine) RenderTo(w io.Writer, name string, context map[string]interface{}) error {
LogInfo("Rendering template to writer: %s", name)
// Store current template name and previous template name
prevTemplate := e.currentTemplate
e.currentTemplate = name
defer func() {
// Restore previous template name when we're done
e.currentTemplate = prevTemplate
}()
template, err := e.Load(name)
if err != nil {
LogError(err, fmt.Sprintf("Failed to load template: %s", name))
return err
}
// If debug is enabled, use more detailed error reporting
if e.environment.debug {
ctx := NewRenderContext(e.environment, context, e)
defer ctx.Release()
// Use debug rendering with enhanced error reporting
err = DebugRender(w, template, ctx)
if err != nil {
LogError(err, fmt.Sprintf("Error rendering template: %s", name))
// Enhance error with template information
if enhancedErr, ok := err.(*EnhancedError); !ok {
err = NewError(err, name, 0, 0, template.source)
} else {
// Already enhanced, just ensure template name is set
if enhancedErr.Template == "" {
enhancedErr.Template = name
}
}
return err
}
return nil
}
// Normal rendering path without debug overhead
return template.RenderTo(w, context)
}
// Load loads a template by name
func (e *Engine) Load(name string) (*Template, error) {
// Only check the cache if caching is enabled
if e.environment.cache {
e.mu.RLock()
tmpl, ok := e.templates[name]
// If template exists in cache
if ok {
// If auto-reload is disabled, return the cached template immediately under read lock
if !e.autoReload {
defer e.mu.RUnlock()
return tmpl, nil
}
// If auto-reload is enabled, check if the template has been modified
needsReload := false
if tmpl.loader != nil {
// Check if the loader supports timestamp checking
if tsLoader, ok := tmpl.loader.(TimestampAwareLoader); ok {
// Get the current modification time
currentModTime, err := tsLoader.GetModifiedTime(name)
if err != nil || currentModTime > tmpl.lastModified {
needsReload = true
}
}
}
// If no reload needed, return cached template
if !needsReload {
defer e.mu.RUnlock()
return tmpl, nil
}
}
// Template not found in cache or needs reloading
e.mu.RUnlock()
}
// Template not in cache or cache disabled or needs reloading
var lastModified int64
var sourceLoader Loader
var loaderErrors []error
for _, loader := range e.loaders {
source, err := loader.Load(name)
if err != nil {
// Collect loader errors for better diagnostics
loaderErrors = append(loaderErrors, fmt.Errorf("loader %T: %w", loader, err))
continue
}
// If this loader supports modification times, get the time
if tsLoader, ok := loader.(TimestampAwareLoader); ok {
lastModified, _ = tsLoader.GetModifiedTime(name)
}
sourceLoader = loader
LogInfo("Template '%s' loaded from %T", name, loader)
parser := &Parser{}
nodes, err := parser.Parse(source)
if err != nil {
// Include more context in parsing errors
return nil, NewError(err, name, 0, 0, source)
}
template := &Template{
name: name,
source: source,
nodes: nodes,
env: e.environment,
engine: e, // Add reference to the engine
loader: sourceLoader,
lastModified: lastModified,
}
// Only cache if caching is enabled
if e.environment.cache {
e.mu.Lock()
e.templates[name] = template
e.mu.Unlock()
}
return template, nil
}
// If we have collected errors from loaders, include them in the error message
if len(loaderErrors) > 0 {
errorDetails := strings.Builder{}
errorDetails.WriteString(fmt.Sprintf("Template '%s' not found. Tried %d loaders:\n", name, len(loaderErrors)))
for i, err := range loaderErrors {
errorDetails.WriteString(fmt.Sprintf(" %d) %s\n", i+1, err.Error()))
}
LogError(ErrTemplateNotFound, errorDetails.String())
return nil, fmt.Errorf("%w: %s", ErrTemplateNotFound, errorDetails.String())
}
LogError(ErrTemplateNotFound, fmt.Sprintf("Template '%s' not found. No loaders configured.", name))
return nil, fmt.Errorf("%w: '%s'", ErrTemplateNotFound, name)
}
// RegisterString registers a template from a string source
func (e *Engine) RegisterString(name string, source string) error {
parser := &Parser{}
nodes, err := parser.Parse(source)
if err != nil {
return err
}
// String templates always use the current time as modification time
// and null loader since they're not loaded from a file
now := time.Now().Unix()
template := &Template{
name: name,
source: source,
nodes: nodes,
env: e.environment,
engine: e,
lastModified: now,
loader: nil, // String templates don't have a loader
}
// Only cache if caching is enabled
if e.environment.cache {
e.mu.Lock()
e.templates[name] = template
e.mu.Unlock()
}
return nil
}
// GetEnvironment returns the engine's environment
func (e *Engine) GetEnvironment() *Environment {
return e.environment
}
// IsDebugEnabled returns true if debug mode is enabled
func (e *Engine) IsDebugEnabled() bool {
return e.environment.debug
}
// IsCacheEnabled returns true if caching is enabled
func (e *Engine) IsCacheEnabled() bool {
return e.environment.cache
}
// IsAutoReloadEnabled returns true if auto-reload is enabled
func (e *Engine) IsAutoReloadEnabled() bool {
return e.autoReload
}
// GetCachedTemplateCount returns the number of templates in the cache
func (e *Engine) GetCachedTemplateCount() int {
e.mu.RLock()
defer e.mu.RUnlock()
return len(e.templates)
}
// GetCachedTemplateNames returns a list of template names in the cache
func (e *Engine) GetCachedTemplateNames() []string {
e.mu.RLock()
defer e.mu.RUnlock()
names := make([]string, 0, len(e.templates))
for name := range e.templates {
names = append(names, name)
}
return names
}
// RegisterTemplate directly registers a pre-built template
func (e *Engine) RegisterTemplate(name string, template *Template) {
// Set the lastModified timestamp if it's not already set
if template.lastModified == 0 {
template.lastModified = time.Now().Unix()
}
// Only cache if caching is enabled
if e.environment.cache {
e.mu.Lock()
e.templates[name] = template
e.mu.Unlock()
}
}
// CompileTemplate compiles a template for faster rendering
func (e *Engine) CompileTemplate(name string) (*CompiledTemplate, error) {
template, err := e.Load(name)
if err != nil {
return nil, err
}
// Compile the template
return CompileTemplate(template)
}
// RegisterCompiledTemplate registers a compiled template with the engine
func (e *Engine) RegisterCompiledTemplate(compiled *CompiledTemplate) error {
if compiled == nil {
return errors.New("cannot register nil compiled template")
}
// Load the template from the compiled representation
template, err := LoadFromCompiled(compiled, e.environment, e)
if err != nil {
return err
}
// Register the template
e.RegisterTemplate(compiled.Name, template)
return nil
}
// LoadFromCompiledData loads a template from serialized compiled data
func (e *Engine) LoadFromCompiledData(data []byte) error {
// Deserialize the compiled template
compiled, err := DeserializeCompiledTemplate(data)
if err != nil {
return err
}
// Register the compiled template
return e.RegisterCompiledTemplate(compiled)
}
// AddFilter registers a custom filter function
func (e *Engine) AddFilter(name string, filter FilterFunc) {
e.environment.filters[name] = filter
}
// AddFunction registers a custom function
func (e *Engine) AddFunction(name string, function FunctionFunc) {
e.environment.functions[name] = function
}
// AddTest registers a custom test function
func (e *Engine) AddTest(name string, test TestFunc) {
e.environment.tests[name] = test
}
// AddGlobal adds a global variable to the template environment
func (e *Engine) AddGlobal(name string, value interface{}) {
e.environment.globals[name] = value
}
// AddExtension registers a Twig extension
func (e *Engine) AddExtension(extension Extension) {
e.environment.extensions = append(e.environment.extensions, extension)
// Register all filters from the extension
for name, filter := range extension.GetFilters() {
e.environment.filters[name] = filter
}
// Register all functions from the extension
for name, function := range extension.GetFunctions() {
e.environment.functions[name] = function
}
// Register all tests from the extension
for name, test := range extension.GetTests() {
e.environment.tests[name] = test
}
// Register all operators from the extension
for name, operator := range extension.GetOperators() {
e.environment.operators[name] = operator
}
// Initialize the extension
extension.Initialize(e)
}
// CreateExtension creates a new custom extension with the given name
func (e *Engine) CreateExtension(name string) *CustomExtension {
extension := &CustomExtension{
Name: name,
Filters: make(map[string]FilterFunc),
Functions: make(map[string]FunctionFunc),
Tests: make(map[string]TestFunc),
Operators: make(map[string]OperatorFunc),
}
return extension
}
// AddFilterToExtension adds a filter to a custom extension
func (e *Engine) AddFilterToExtension(extension *CustomExtension, name string, filter FilterFunc) {
if extension.Filters == nil {
extension.Filters = make(map[string]FilterFunc)
}
extension.Filters[name] = filter
}
// AddFunctionToExtension adds a function to a custom extension
func (e *Engine) AddFunctionToExtension(extension *CustomExtension, name string, function FunctionFunc) {
if extension.Functions == nil {
extension.Functions = make(map[string]FunctionFunc)
}
extension.Functions[name] = function
}
// AddTestToExtension adds a test to a custom extension
func (e *Engine) AddTestToExtension(extension *CustomExtension, name string, test TestFunc) {
if extension.Tests == nil {
extension.Tests = make(map[string]TestFunc)
}
extension.Tests[name] = test
}
// RegisterExtension creates, configures and registers a custom extension
func (e *Engine) RegisterExtension(name string, config func(*CustomExtension)) {
extension := e.CreateExtension(name)
if config != nil {
config(extension)
}
e.AddExtension(extension)
}
// NewTemplate creates a new template with the given parameters
func (e *Engine) NewTemplate(name string, source string, nodes Node) *Template {
return &Template{
name: name,
source: source,
nodes: nodes,
env: e.environment,
engine: e,
lastModified: time.Now().Unix(),
loader: nil,
}
}
// ParseTemplate parses a template string
func (e *Engine) ParseTemplate(source string) (*Template, error) {
// Use the override Parse function if it's set (for testing)
if e.Parse != nil {
return e.Parse(source)
}
parser := &Parser{}
nodes, err := parser.Parse(source)
if err != nil {
return nil, err
}
template := &Template{
source: source,
nodes: nodes,
env: e.environment,
engine: e,
lastModified: time.Now().Unix(),
loader: nil,
}
return template, nil
}
// Render renders a template with the given context
func (t *Template) Render(context map[string]interface{}) (string, error) {
// Get a string buffer from the pool
buf := NewStringBuffer()
defer buf.Release()
err := t.RenderTo(buf, context)
if err != nil {
return "", err
}
return buf.String(), nil
}
// RenderTo renders a template to a writer
func (t *Template) RenderTo(w io.Writer, context map[string]interface{}) error {
// Get a render context from the pool
ctx := NewRenderContext(t.env, context, t.engine)
// Debug logging only when enabled
LogDebug("Rendering template '%s'", t.name)
// Ensure the context is returned to the pool
defer ctx.Release()
return t.nodes.Render(w, ctx)
}
// Compile compiles the template to a CompiledTemplate
func (t *Template) Compile() (*CompiledTemplate, error) {
return CompileTemplate(t)
}
// SaveCompiled serializes the compiled template to a byte array
func (t *Template) SaveCompiled() ([]byte, error) {
// Compile the template
compiled, err := t.Compile()
if err != nil {
return nil, err
}
// Serialize the compiled template
return SerializeCompiledTemplate(compiled)
}
// StringBuffer is a simple buffer for string building
type StringBuffer struct {
buf bytes.Buffer
}
// stringBufferPool is a sync.Pool for StringBuffer objects
var stringBufferPool = sync.Pool{
New: func() interface{} {
return &StringBuffer{}
},
}
// NewStringBuffer gets a StringBuffer from the pool
func NewStringBuffer() *StringBuffer {
buffer := stringBufferPool.Get().(*StringBuffer)
buffer.buf.Reset() // Clear any previous content
return buffer
}
// Release returns the StringBuffer to the pool
func (b *StringBuffer) Release() {
stringBufferPool.Put(b)
}
// Write implements io.Writer
func (b *StringBuffer) Write(p []byte) (n int, err error) {
return b.buf.Write(p)
}
// String returns the buffer's contents as a string
func (b *StringBuffer) String() string {
return b.buf.String()
}