go-twig/loader.go
semihalev f9b283c393 Add template compilation capabilities
- 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>
2025-03-10 09:21:20 +03:00

241 lines
5.9 KiB
Go

package twig
import (
"fmt"
"os"
"path/filepath"
)
// Loader defines the interface for template loading
type Loader interface {
// Load loads a template by name, returning its source code
Load(name string) (string, error)
// Exists checks if a template exists
Exists(name string) bool
}
// TimestampAwareLoader is an interface for loaders that can check modification times
type TimestampAwareLoader interface {
Loader
// GetModifiedTime returns the last modification time of a template
GetModifiedTime(name string) (int64, error)
}
// FileSystemLoader loads templates from the file system
type FileSystemLoader struct {
paths []string
suffix string
defaultPaths []string
// Stores paths for each loaded template to avoid repeatedly searching for the file
templatePaths map[string]string
}
// ArrayLoader loads templates from an in-memory array
type ArrayLoader struct {
templates map[string]string
}
// ChainLoader chains multiple loaders together
type ChainLoader struct {
loaders []Loader
}
// NewFileSystemLoader creates a new file system loader
func NewFileSystemLoader(paths []string) *FileSystemLoader {
// Add default path
defaultPaths := []string{"."}
// If no paths provided, use default
if len(paths) == 0 {
paths = defaultPaths
}
// Normalize paths
normalizedPaths := make([]string, len(paths))
for i, path := range paths {
normalizedPaths[i] = filepath.Clean(path)
}
return &FileSystemLoader{
paths: normalizedPaths,
suffix: ".twig",
defaultPaths: defaultPaths,
templatePaths: make(map[string]string),
}
}
// Load loads a template from the file system
func (l *FileSystemLoader) Load(name string) (string, error) {
// Check if we already know the location of this template
if filePath, ok := l.templatePaths[name]; ok {
// Check if file still exists at this path
if _, err := os.Stat(filePath); err == nil {
// Read file content
content, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("error reading template %s: %w", name, err)
}
return string(content), nil
}
// If file doesn't exist anymore, remove from cache and search again
delete(l.templatePaths, name)
}
// Check each path for the template
for _, path := range l.paths {
filePath := filepath.Join(path, name)
// Add suffix if not already present
if !hasSuffix(filePath, l.suffix) {
filePath = filePath + l.suffix
}
// Check if file exists
if _, err := os.Stat(filePath); err == nil {
// Save the path for future lookups
l.templatePaths[name] = filePath
// Read file content
content, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("error reading template %s: %w", name, err)
}
return string(content), nil
}
}
return "", fmt.Errorf("%w: %s", ErrTemplateNotFound, name)
}
// Exists checks if a template exists in the file system
func (l *FileSystemLoader) Exists(name string) bool {
// Check each path for the template
for _, path := range l.paths {
filePath := filepath.Join(path, name)
// Add suffix if not already present
if !hasSuffix(filePath, l.suffix) {
filePath = filePath + l.suffix
}
// Check if file exists
if _, err := os.Stat(filePath); err == nil {
return true
}
}
return false
}
// SetSuffix sets the file suffix for templates
func (l *FileSystemLoader) SetSuffix(suffix string) {
l.suffix = suffix
}
// GetModifiedTime returns the last modification time of a template file
func (l *FileSystemLoader) GetModifiedTime(name string) (int64, error) {
// If we already know where this template is, check that path directly
if filePath, ok := l.templatePaths[name]; ok {
info, err := os.Stat(filePath)
if err != nil {
// If file doesn't exist anymore, remove from cache
if os.IsNotExist(err) {
delete(l.templatePaths, name)
}
return 0, err
}
return info.ModTime().Unix(), nil
}
// Otherwise search for the template
for _, path := range l.paths {
filePath := filepath.Join(path, name)
// Add suffix if not already present
if !hasSuffix(filePath, l.suffix) {
filePath = filePath + l.suffix
}
// Check if file exists
info, err := os.Stat(filePath)
if err == nil {
// Save the path for future lookups
l.templatePaths[name] = filePath
return info.ModTime().Unix(), nil
}
}
return 0, fmt.Errorf("%w: %s", ErrTemplateNotFound, name)
}
// NewArrayLoader creates a new array loader
func NewArrayLoader(templates map[string]string) *ArrayLoader {
return &ArrayLoader{
templates: templates,
}
}
// Load loads a template from the array
func (l *ArrayLoader) Load(name string) (string, error) {
if template, ok := l.templates[name]; ok {
return template, nil
}
return "", fmt.Errorf("%w: %s", ErrTemplateNotFound, name)
}
// Exists checks if a template exists in the array
func (l *ArrayLoader) Exists(name string) bool {
_, ok := l.templates[name]
return ok
}
// SetTemplate adds or updates a template in the array
func (l *ArrayLoader) SetTemplate(name, template string) {
l.templates[name] = template
}
// NewChainLoader creates a new chain loader
func NewChainLoader(loaders []Loader) *ChainLoader {
return &ChainLoader{
loaders: loaders,
}
}
// Load loads a template from the first loader that has it
func (l *ChainLoader) Load(name string) (string, error) {
for _, loader := range l.loaders {
if loader.Exists(name) {
return loader.Load(name)
}
}
return "", fmt.Errorf("%w: %s", ErrTemplateNotFound, name)
}
// Exists checks if a template exists in any of the loaders
func (l *ChainLoader) Exists(name string) bool {
for _, loader := range l.loaders {
if loader.Exists(name) {
return true
}
}
return false
}
// AddLoader adds a loader to the chain
func (l *ChainLoader) AddLoader(loader Loader) {
l.loaders = append(l.loaders, loader)
}
// Helper function to check if a string has a suffix
func hasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}