mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
- Implement a compiled template format using gob encoding - Add methods to compile templates and load from compiled templates - Create dedicated CompiledLoader for managing compiled templates - Enable auto-reload support for compiled templates - Add comprehensive tests including benchmarks - Create example application for template compilation workflow - Update documentation with compilation features and examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
523 lines
13 KiB
Go
523 lines
13 KiB
Go
//go:generate go run tools/lexgen/main.go -output gen
|
|
//go:generate go run tools/parsegen/main.go -output gen
|
|
package twig
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"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
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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.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) {
|
|
template, err := e.Load(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
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 {
|
|
template, err := e.Load(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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()
|
|
if tmpl, ok := e.templates[name]; ok {
|
|
e.mu.RUnlock()
|
|
|
|
// If auto-reload is disabled, return the cached template
|
|
if !e.autoReload {
|
|
return tmpl, nil
|
|
}
|
|
|
|
// If auto-reload is enabled, check if the template has been modified
|
|
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 {
|
|
// Template hasn't been modified, use the cached version
|
|
return tmpl, nil
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
e.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// Template not in cache or cache disabled or needs reloading
|
|
var lastModified int64
|
|
var sourceLoader Loader
|
|
|
|
for _, loader := range e.loaders {
|
|
source, err := loader.Load(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// If this loader supports modification times, get the time
|
|
if tsLoader, ok := loader.(TimestampAwareLoader); ok {
|
|
lastModified, _ = tsLoader.GetModifiedTime(name)
|
|
}
|
|
|
|
sourceLoader = loader
|
|
|
|
parser := &Parser{}
|
|
nodes, err := parser.Parse(source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return nil, ErrTemplateNotFound
|
|
}
|
|
|
|
// 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) {
|
|
var buf StringBuffer
|
|
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 {
|
|
if context == nil {
|
|
context = make(map[string]interface{})
|
|
}
|
|
|
|
// Create a render context with access to the engine
|
|
ctx := &RenderContext{
|
|
env: t.env,
|
|
context: context,
|
|
blocks: make(map[string][]Node),
|
|
macros: make(map[string]Node),
|
|
engine: t.engine,
|
|
extending: false,
|
|
currentBlock: nil,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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()
|
|
}
|