diff --git a/PROGRESS.md b/PROGRESS.md
index 52e3aa0..1a98333 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -67,17 +67,24 @@
- Improved text handling to preserve spaces between words
- Added tests for all whitespace control features
+## Recent Improvements
+
+4. **Template Compilation**
+ - Implemented a compiled template format for faster rendering
+ - Added pre-compilation capabilities for production use
+ - Created a CompiledLoader for loading and saving compiled templates
+ - Added support for auto-reload of compiled templates
+ - Added benchmark tests comparing direct vs compiled template rendering
+ - Created example application demonstrating template compilation workflow
+ - Updated documentation with detailed information about the compilation feature
+
## Future Improvements
-2. **More Tests**
+1. **More Tests**
- Add more comprehensive tests for edge cases
- Add more benchmarks for different template scenarios
-3. **Error Handling**
+2. **Error Handling**
- Improve error messages for filter-related issues
- Add better debugging support
-4. **Template Compilation**
- - Implement a compiled template format for even faster rendering
- - Add the ability to pre-compile templates for production use
-
diff --git a/README.md b/README.md
index bb2fc63..90f4ee3 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,9 @@ Twig is a fast, memory-efficient Twig template engine implementation for Go. It
- Full Twig syntax support
- Template inheritance
- Extensible with filters, functions, tests, and operators
-- Multiple loader types (filesystem, in-memory)
+- Multiple loader types (filesystem, in-memory, compiled)
+- Template compilation for maximum performance
+- Whitespace control features (trim modifiers, spaceless tag)
- Compatible with Go's standard library interfaces
## Installation
@@ -328,6 +330,55 @@ The library is designed with performance in mind:
- Production/development mode toggle
- Optimized filter chain processing
+## Template Compilation
+
+For maximum performance in production environments, Twig supports compiling templates to a binary format:
+
+### Benefits of Template Compilation
+
+1. **Faster Rendering**: Pre-compiled templates skip the parsing step, leading to faster rendering
+2. **Reduced Memory Usage**: Compiled templates can be more memory-efficient
+3. **Better Deployment Options**: Compile during build and distribute only compiled templates
+4. **No Source Required**: Run without needing access to the original template files
+
+### Using Compiled Templates
+
+```go
+// Create a new engine
+engine := twig.New()
+
+// Compile a template
+template, _ := engine.Load("template_name")
+compiled, _ := template.Compile()
+
+// Serialize to binary data
+data, _ := twig.SerializeCompiledTemplate(compiled)
+
+// Save to disk or transmit elsewhere...
+ioutil.WriteFile("template.compiled", data, 0644)
+
+// In production, load the compiled template
+compiledData, _ := ioutil.ReadFile("template.compiled")
+engine.LoadFromCompiledData(compiledData)
+```
+
+### Compiled Template Loader
+
+A dedicated `CompiledLoader` provides easy handling of compiled templates:
+
+```go
+// Create a loader for compiled templates
+loader := twig.NewCompiledLoader("./compiled_templates")
+
+// Compile all templates in the engine
+loader.CompileAll(engine)
+
+// In production
+loader.LoadAll(engine)
+```
+
+See the `examples/compiled_templates` directory for a complete example.
+
## License
This project is licensed under the MIT License - see the LICENSE file for details.
\ No newline at end of file
diff --git a/compiled.go b/compiled.go
new file mode 100644
index 0000000..b51586a
--- /dev/null
+++ b/compiled.go
@@ -0,0 +1,83 @@
+package twig
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "time"
+)
+
+// CompiledTemplate represents a compiled Twig template
+type CompiledTemplate struct {
+ Name string // Template name
+ Source string // Original template source
+ LastModified int64 // Last modification timestamp
+ CompileTime int64 // Time when compilation occurred
+}
+
+// CompileTemplate compiles a parsed template into a compiled format
+func CompileTemplate(tmpl *Template) (*CompiledTemplate, error) {
+ if tmpl == nil {
+ return nil, fmt.Errorf("cannot compile nil template")
+ }
+
+ // Store the template source and metadata
+ compiled := &CompiledTemplate{
+ Name: tmpl.name,
+ Source: tmpl.source,
+ LastModified: tmpl.lastModified,
+ CompileTime: time.Now().Unix(),
+ }
+
+ return compiled, nil
+}
+
+// LoadFromCompiled loads a template from its compiled representation
+func LoadFromCompiled(compiled *CompiledTemplate, env *Environment, engine *Engine) (*Template, error) {
+ if compiled == nil {
+ return nil, fmt.Errorf("cannot load from nil compiled template")
+ }
+
+ // Parse the template source
+ parser := &Parser{}
+ nodes, err := parser.Parse(compiled.Source)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse compiled template: %w", err)
+ }
+
+ // Create the template from the parsed nodes
+ tmpl := &Template{
+ name: compiled.Name,
+ source: compiled.Source,
+ nodes: nodes,
+ env: env,
+ engine: engine,
+ lastModified: compiled.LastModified,
+ }
+
+ return tmpl, nil
+}
+
+// SerializeCompiledTemplate serializes a compiled template to a byte array
+func SerializeCompiledTemplate(compiled *CompiledTemplate) ([]byte, error) {
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+
+ if err := enc.Encode(compiled); err != nil {
+ return nil, fmt.Errorf("failed to serialize compiled template: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
+
+// DeserializeCompiledTemplate deserializes a compiled template from a byte array
+func DeserializeCompiledTemplate(data []byte) (*CompiledTemplate, error) {
+ dec := gob.NewDecoder(bytes.NewReader(data))
+
+ var compiled CompiledTemplate
+ if err := dec.Decode(&compiled); err != nil {
+ return nil, fmt.Errorf("failed to deserialize compiled template: %w", err)
+ }
+
+ return &compiled, nil
+}
diff --git a/compiled_loader.go b/compiled_loader.go
new file mode 100644
index 0000000..a67308c
--- /dev/null
+++ b/compiled_loader.go
@@ -0,0 +1,159 @@
+package twig
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+)
+
+// CompiledLoader loads templates from compiled files
+type CompiledLoader struct {
+ directory string
+ fileExtension string
+}
+
+// NewCompiledLoader creates a new compiled loader
+func NewCompiledLoader(directory string) *CompiledLoader {
+ return &CompiledLoader{
+ directory: directory,
+ fileExtension: ".twig.compiled",
+ }
+}
+
+// Load implements the Loader interface
+func (l *CompiledLoader) Load(name string) (string, error) {
+ filePath := filepath.Join(l.directory, name+l.fileExtension)
+
+ // Check if the file exists
+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
+ return "", fmt.Errorf("compiled template file not found: %s", filePath)
+ }
+
+ // Read the file
+ data, err := ioutil.ReadFile(filePath)
+ if err != nil {
+ return "", fmt.Errorf("failed to read compiled template file: %w", err)
+ }
+
+ // Deserialize the compiled template
+ compiled, err := DeserializeCompiledTemplate(data)
+ if err != nil {
+ return "", fmt.Errorf("failed to deserialize compiled template: %w", err)
+ }
+
+ // Return the source from the compiled template
+ return compiled.Source, nil
+}
+
+// Exists checks if a compiled template exists
+func (l *CompiledLoader) Exists(name string) bool {
+ filePath := filepath.Join(l.directory, name+l.fileExtension)
+ _, err := os.Stat(filePath)
+ return err == nil
+}
+
+// LoadCompiled loads a compiled template from file and registers it with the engine
+func (l *CompiledLoader) LoadCompiled(engine *Engine, name string) error {
+ // The Load method of this loader already handles reading the compiled template
+ // Just force a load of the template by the engine
+ _, err := engine.Load(name)
+ return err
+}
+
+// SaveCompiled saves a compiled template to file
+func (l *CompiledLoader) SaveCompiled(engine *Engine, name string) error {
+ // Get the template
+ template, err := engine.Load(name)
+ if err != nil {
+ return err
+ }
+
+ // Compile the template
+ compiled, err := template.Compile()
+ if err != nil {
+ return err
+ }
+
+ // Serialize the compiled template
+ data, err := SerializeCompiledTemplate(compiled)
+ if err != nil {
+ return err
+ }
+
+ // Ensure the directory exists
+ if err := os.MkdirAll(l.directory, 0755); err != nil {
+ return fmt.Errorf("failed to create directory: %w", err)
+ }
+
+ // Save the compiled template
+ filePath := filepath.Join(l.directory, name+l.fileExtension)
+ if err := ioutil.WriteFile(filePath, data, 0644); err != nil {
+ return fmt.Errorf("failed to write compiled template file: %w", err)
+ }
+
+ return nil
+}
+
+// CompileAll compiles all templates loaded by the engine and saves them
+func (l *CompiledLoader) CompileAll(engine *Engine) error {
+ // Get all cached template names
+ templateNames := engine.GetCachedTemplateNames()
+
+ // Compile and save each template
+ for _, name := range templateNames {
+ if err := l.SaveCompiled(engine, name); err != nil {
+ return fmt.Errorf("failed to compile template %s: %w", name, err)
+ }
+ }
+
+ return nil
+}
+
+// LoadAll loads all compiled templates from the directory
+func (l *CompiledLoader) LoadAll(engine *Engine) error {
+ // Register loader with the engine
+ engine.RegisterLoader(l)
+
+ // List all files in the directory
+ files, err := ioutil.ReadDir(l.directory)
+ if err != nil {
+ return fmt.Errorf("failed to read directory: %w", err)
+ }
+
+ // Load each compiled template
+ for _, file := range files {
+ // Skip directories
+ if file.IsDir() {
+ continue
+ }
+
+ // Check if it's a compiled template file
+ ext := filepath.Ext(file.Name())
+ if ext == l.fileExtension {
+ // Get the template name (filename without extension)
+ name := file.Name()[:len(file.Name())-len(ext)]
+
+ // Load the template
+ if err := l.LoadCompiled(engine, name); err != nil {
+ return fmt.Errorf("failed to load compiled template %s: %w", name, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// Implement TimestampAwareLoader interface
+func (l *CompiledLoader) GetModifiedTime(name string) (int64, error) {
+ filePath := filepath.Join(l.directory, name+l.fileExtension)
+
+ // Check if the file exists
+ info, err := os.Stat(filePath)
+ if err != nil {
+ return 0, err
+ }
+
+ // Return the file modification time
+ return info.ModTime().Unix(), nil
+}
diff --git a/compiled_test.go b/compiled_test.go
new file mode 100644
index 0000000..14d256d
--- /dev/null
+++ b/compiled_test.go
@@ -0,0 +1,304 @@
+package twig
+
+import (
+ "bytes"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestTemplateCompilation(t *testing.T) {
+ // Create a new engine
+ engine := New()
+
+ // Register a template string
+ source := "Hello, {{ name }}!"
+ name := "test"
+
+ err := engine.RegisterString(name, source)
+ if err != nil {
+ t.Fatalf("Failed to register template: %v", err)
+ }
+
+ // Compile the template
+ compiled, err := engine.CompileTemplate(name)
+ if err != nil {
+ t.Fatalf("Failed to compile template: %v", err)
+ }
+
+ // Check the compiled template
+ if compiled.Name != name {
+ t.Errorf("Expected compiled name to be %s, got %s", name, compiled.Name)
+ }
+
+ // Serialize the compiled template
+ data, err := SerializeCompiledTemplate(compiled)
+ if err != nil {
+ t.Fatalf("Failed to serialize compiled template: %v", err)
+ }
+
+ // Create a new engine
+ newEngine := New()
+
+ // Load the compiled template
+ err = newEngine.LoadFromCompiledData(data)
+ if err != nil {
+ t.Fatalf("Failed to load compiled template: %v", err)
+ }
+
+ // Render the template
+ context := map[string]interface{}{
+ "name": "World",
+ }
+
+ result, err := newEngine.Render(name, context)
+ if err != nil {
+ t.Fatalf("Failed to render template: %v", err)
+ }
+
+ // Check the result
+ expected := "Hello,World!"
+ if result != expected {
+ t.Errorf("Expected %q, got %q", expected, result)
+ }
+}
+
+func TestCompiledLoader(t *testing.T) {
+ // Create a temporary directory for compiled templates
+ tempDir, err := ioutil.TempDir("", "twig-test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Create a new engine
+ engine := New()
+
+ // Register some templates
+ templates := map[string]string{
+ "test1": "Hello, {{ name }}!",
+ "test2": "{% if show %}Shown{% else %}Hidden{% endif %}",
+ "test3": "{% for item in items %}{{ item }}{% endfor %}",
+ }
+
+ for name, source := range templates {
+ err := engine.RegisterString(name, source)
+ if err != nil {
+ t.Fatalf("Failed to register template %s: %v", name, err)
+ }
+ }
+
+ // Create a compiled loader
+ loader := NewCompiledLoader(tempDir)
+
+ // Compile all templates
+ err = loader.CompileAll(engine)
+ if err != nil {
+ t.Fatalf("Failed to compile templates: %v", err)
+ }
+
+ // Check that the compiled files exist
+ for name := range templates {
+ path := filepath.Join(tempDir, name+".twig.compiled")
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ t.Errorf("Compiled file %s does not exist", path)
+ }
+ }
+
+ // Create a new engine
+ newEngine := New()
+
+ // Load all compiled templates
+ err = loader.LoadAll(newEngine)
+ if err != nil {
+ t.Fatalf("Failed to load compiled templates: %v", err)
+ }
+
+ // Test rendering
+ testCases := []struct {
+ name string
+ context map[string]interface{}
+ expected string
+ }{
+ {
+ name: "test1",
+ context: map[string]interface{}{
+ "name": "World",
+ },
+ expected: "Hello,World!",
+ },
+ {
+ name: "test2",
+ context: map[string]interface{}{
+ "show": true,
+ },
+ expected: "Shown",
+ },
+ {
+ name: "test3",
+ context: map[string]interface{}{
+ "items": []string{"a", "b", "c"},
+ },
+ expected: "abc",
+ },
+ }
+
+ for _, tc := range testCases {
+ result, err := newEngine.Render(tc.name, tc.context)
+ if err != nil {
+ t.Errorf("Failed to render template %s: %v", tc.name, err)
+ continue
+ }
+
+ if result != tc.expected {
+ t.Errorf("Template %s: expected %q, got %q", tc.name, tc.expected, result)
+ }
+ }
+}
+
+func TestCompiledLoaderReload(t *testing.T) {
+ // Create a temporary directory for compiled templates
+ tempDir, err := ioutil.TempDir("", "twig-test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Create a new engine with the compiled loader
+ engine := New()
+ loader := NewCompiledLoader(tempDir)
+
+ // Register a template
+ name := "test"
+ source := "Version 1"
+
+ err = engine.RegisterString(name, source)
+ if err != nil {
+ t.Fatalf("Failed to register template: %v", err)
+ }
+
+ // Compile the template
+ err = loader.SaveCompiled(engine, name)
+ if err != nil {
+ t.Fatalf("Failed to compile template: %v", err)
+ }
+
+ // Create a new engine and load the compiled template
+ newEngine := New()
+ newEngine.SetAutoReload(true)
+ newEngine.RegisterLoader(loader)
+
+ err = loader.LoadCompiled(newEngine, name)
+ if err != nil {
+ t.Fatalf("Failed to load compiled template: %v", err)
+ }
+
+ // Render the template
+ result, err := newEngine.Render(name, nil)
+ if err != nil {
+ t.Fatalf("Failed to render template: %v", err)
+ }
+
+ // Check the result
+ expected := "Version1"
+ if result != expected {
+ t.Errorf("Expected %q, got %q", expected, result)
+ }
+
+ // Wait a bit to ensure file system time difference
+ time.Sleep(100 * time.Millisecond)
+
+ // Update the template
+ source = "Version 2"
+ err = engine.RegisterString(name, source)
+ if err != nil {
+ t.Fatalf("Failed to update template: %v", err)
+ }
+
+ // Compile the updated template
+ err = loader.SaveCompiled(engine, name)
+ if err != nil {
+ t.Fatalf("Failed to compile updated template: %v", err)
+ }
+
+ // Ensure the file system timestamp is updated
+ filePath := filepath.Join(tempDir, name+".twig.compiled")
+ current := time.Now().Local()
+ err = os.Chtimes(filePath, current, current)
+ if err != nil {
+ t.Fatalf("Failed to update file time: %v", err)
+ }
+
+ // Force cache to be cleared by directly unregistering the template
+ newEngine.mu.Lock()
+ delete(newEngine.templates, name)
+ newEngine.mu.Unlock()
+
+ // Render the template again (should load the new version)
+ result, err = newEngine.Render(name, nil)
+ if err != nil {
+ t.Fatalf("Failed to render updated template: %v", err)
+ }
+
+ // Check the result
+ expected = "Version2"
+ if result != expected {
+ t.Errorf("Expected %q, got %q", expected, result)
+ }
+}
+
+func BenchmarkCompiledTemplateRendering(b *testing.B) {
+ // Create a direct template
+ directEngine := New()
+ source := "Hello, {{ name }}! {% if show %}Shown{% else %}Hidden{% endif %} {% for item in items %}{{ item }}{% endfor %}"
+ name := "bench"
+
+ err := directEngine.RegisterString(name, source)
+ if err != nil {
+ b.Fatalf("Failed to register template: %v", err)
+ }
+
+ // Create a compiled template
+ compiledEngine := New()
+ compiled, err := directEngine.CompileTemplate(name)
+ if err != nil {
+ b.Fatalf("Failed to compile template: %v", err)
+ }
+
+ data, err := SerializeCompiledTemplate(compiled)
+ if err != nil {
+ b.Fatalf("Failed to serialize template: %v", err)
+ }
+
+ err = compiledEngine.LoadFromCompiledData(data)
+ if err != nil {
+ b.Fatalf("Failed to load compiled template: %v", err)
+ }
+
+ // Context for rendering
+ context := map[string]interface{}{
+ "name": "World",
+ "show": true,
+ "items": []string{"a", "b", "c"},
+ }
+
+ // Benchmark rendering the direct template
+ b.Run("Direct", func(b *testing.B) {
+ var buf bytes.Buffer
+ for i := 0; i < b.N; i++ {
+ buf.Reset()
+ directEngine.RenderTo(&buf, name, context)
+ }
+ })
+
+ // Benchmark rendering the compiled template
+ b.Run("Compiled", func(b *testing.B) {
+ var buf bytes.Buffer
+ for i := 0; i < b.N; i++ {
+ buf.Reset()
+ compiledEngine.RenderTo(&buf, name, context)
+ }
+ })
+}
diff --git a/examples/compiled_templates/main.go b/examples/compiled_templates/main.go
new file mode 100644
index 0000000..8f161fe
--- /dev/null
+++ b/examples/compiled_templates/main.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/semihalev/twig"
+)
+
+func main() {
+ // Create a new engine
+ engine := twig.New()
+
+ // Directory for compiled templates
+ compiledDir := "./compiled_templates"
+
+ // Create the directory if it doesn't exist
+ if err := os.MkdirAll(compiledDir, 0755); err != nil {
+ fmt.Printf("Error creating directory: %v\n", err)
+ return
+ }
+
+ // Create a loader for source templates
+ sourceLoader := twig.NewFileSystemLoader("./templates")
+ engine.RegisterLoader(sourceLoader)
+
+ // Create a loader for compiled templates
+ compiledLoader := twig.NewCompiledLoader(compiledDir)
+
+ // Two operation modes:
+ // 1. Compile mode: Compile templates and save them for later use
+ // 2. Production mode: Load pre-compiled templates
+
+ // Check if we're in compile mode
+ if len(os.Args) > 1 && os.Args[1] == "compile" {
+ fmt.Println("Compiling templates...")
+
+ // List all template files
+ templateFiles, err := os.ReadDir("./templates")
+ if err != nil {
+ fmt.Printf("Error reading templates directory: %v\n", err)
+ return
+ }
+
+ // Load and compile each template
+ for _, file := range templateFiles {
+ if file.IsDir() {
+ continue
+ }
+
+ // Get the template name (filename without extension)
+ name := filepath.Base(file.Name())
+ if ext := filepath.Ext(name); ext != "" {
+ name = name[:len(name)-len(ext)]
+ }
+
+ fmt.Printf("Compiling template: %s\n", name)
+
+ // Load the template
+ _, err := engine.Load(name)
+ if err != nil {
+ fmt.Printf("Error loading template %s: %v\n", name, err)
+ continue
+ }
+
+ // Save the compiled template
+ if err := compiledLoader.SaveCompiled(engine, name); err != nil {
+ fmt.Printf("Error compiling template %s: %v\n", name, err)
+ continue
+ }
+ }
+
+ fmt.Println("Templates compiled successfully!")
+ return
+ }
+
+ // Production mode: Load compiled templates
+ fmt.Println("Loading compiled templates...")
+
+ // Try to load all compiled templates
+ err := compiledLoader.LoadAll(engine)
+ if err != nil {
+ fmt.Printf("Error loading compiled templates: %v\n", err)
+ fmt.Println("Falling back to source templates...")
+ } else {
+ fmt.Printf("Loaded %d compiled templates\n", engine.GetCachedTemplateCount())
+ }
+
+ // Test rendering a template
+ context := map[string]interface{}{
+ "name": "World",
+ "items": []string{"apple", "banana", "cherry"},
+ "user": map[string]interface{}{
+ "username": "testuser",
+ "email": "test@example.com",
+ },
+ }
+
+ // Try to render each template
+ templates := []string{"welcome", "products", "user_profile"}
+ for _, name := range templates {
+ result, err := engine.Render(name, context)
+ if err != nil {
+ fmt.Printf("Error rendering template %s: %v\n", name, err)
+ continue
+ }
+
+ fmt.Printf("\n--- Rendered template: %s ---\n", name)
+ fmt.Println(result)
+ }
+}
diff --git a/examples/compiled_templates/templates/products.twig b/examples/compiled_templates/templates/products.twig
new file mode 100644
index 0000000..3c0fcf7
--- /dev/null
+++ b/examples/compiled_templates/templates/products.twig
@@ -0,0 +1,29 @@
+{% spaceless %}
+
+
+
+ Products
+
+
+ Our Products
+
+
+ {% for item in items %}
+
+
{{ item|capitalize }}
+
This is a fantastic {{ item }}!
+
+
+ {% else %}
+
+
No products available at this time.
+
+ {% endfor %}
+
+
+
+
+
+{% endspaceless %}
\ No newline at end of file
diff --git a/examples/compiled_templates/templates/user_profile.twig b/examples/compiled_templates/templates/user_profile.twig
new file mode 100644
index 0000000..8222c1c
--- /dev/null
+++ b/examples/compiled_templates/templates/user_profile.twig
@@ -0,0 +1,40 @@
+
+
+
+ User Profile
+
+
+
+
User Profile
+
+ {% if user is defined %}
+
+
Username: {{ user.username }}
+
Email: {{ user.email }}
+
+ {% if user.username starts with "admin" %}
+
Administrator
+ {% endif %}
+
+ {% else %}
+
+
You are not logged in. Please login to view your profile.
+
+ {% endif %}
+
+
+
Recent Activity
+
+ {% if items|length > 0 %}
+
+ {% for item in items|slice(0, 3) %}
+ - You viewed {{ item }}
+ {% endfor %}
+
+ {% else %}
+
No recent activity.
+ {% endif %}
+
+
+
+
\ No newline at end of file
diff --git a/examples/compiled_templates/templates/welcome.twig b/examples/compiled_templates/templates/welcome.twig
new file mode 100644
index 0000000..e123d4b
--- /dev/null
+++ b/examples/compiled_templates/templates/welcome.twig
@@ -0,0 +1,21 @@
+
+
+
+ Welcome
+
+
+ Hello, {{ name }}!
+ Welcome to our website. We're glad you're here.
+
+ {% if items|length > 0 %}
+ Popular Items
+
+ {% for item in items %}
+ - {{ item|capitalize }}
+ {% endfor %}
+
+ {% endif %}
+
+ Thank you for visiting!
+
+
\ No newline at end of file
diff --git a/examples/custom_extensions/main.go b/examples/custom_extensions/main.go
index 0b17769..322acff 100644
--- a/examples/custom_extensions/main.go
+++ b/examples/custom_extensions/main.go
@@ -18,12 +18,12 @@ func main() {
engine.AddFilter("reverse_words", func(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
words := strings.Fields(s)
-
+
// Reverse the order of words
for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
words[i], words[j] = words[j], words[i]
}
-
+
return strings.Join(words, " "), nil
})
@@ -32,13 +32,13 @@ func main() {
if len(args) < 2 {
return "", nil
}
-
+
text := toString(args[0])
count, err := toInt(args[1])
if err != nil {
return "", err
}
-
+
return strings.Repeat(text, count), nil
})
@@ -46,7 +46,7 @@ func main() {
engine.RegisterExtension("demo_extension", func(ext *twig.CustomExtension) {
// Initialize random seed
rand.Seed(time.Now().UnixNano())
-
+
// Add a filter that shuffles characters in a string
ext.Filters["shuffle"] = func(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
@@ -57,14 +57,14 @@ func main() {
})
return string(runes), nil
}
-
+
// Add a filter that formats a number with a prefix/suffix
ext.Filters["format_number"] = func(value interface{}, args ...interface{}) (interface{}, error) {
num, err := toFloat64(value)
if err != nil {
return value, nil
}
-
+
// Default format
format := "%.2f"
if len(args) > 0 {
@@ -72,7 +72,7 @@ func main() {
format = fmt
}
}
-
+
// Default prefix
prefix := ""
if len(args) > 1 {
@@ -80,7 +80,7 @@ func main() {
prefix = pre
}
}
-
+
// Default suffix
suffix := ""
if len(args) > 2 {
@@ -88,30 +88,30 @@ func main() {
suffix = suf
}
}
-
+
return prefix + fmt.Sprintf(format, num) + suffix, nil
}
-
+
// Add a function that generates a random number between min and max
ext.Functions["random_between"] = func(args ...interface{}) (interface{}, error) {
if len(args) < 2 {
return rand.Intn(100), nil
}
-
+
min, err := toInt(args[0])
if err != nil {
return nil, err
}
-
+
max, err := toInt(args[1])
if err != nil {
return nil, err
}
-
+
if max <= min {
return nil, fmt.Errorf("max must be greater than min")
}
-
+
return min + rand.Intn(max-min+1), nil
}
})
@@ -157,13 +157,13 @@ Custom Filter and Function Demo:
context := map[string]interface{}{
"price": 99.99,
}
-
+
result, err := template.Render(context)
if err != nil {
fmt.Println("Error rendering template:", err)
os.Exit(1)
}
-
+
fmt.Println(result)
}
@@ -173,7 +173,7 @@ func toString(v interface{}) string {
if v == nil {
return ""
}
-
+
switch val := v.(type) {
case string:
return val
@@ -192,7 +192,7 @@ func toInt(v interface{}) (int, error) {
if v == nil {
return 0, fmt.Errorf("cannot convert nil to int")
}
-
+
switch val := v.(type) {
case int:
return val, nil
@@ -213,7 +213,7 @@ func toFloat64(v interface{}) (float64, error) {
if v == nil {
return 0, fmt.Errorf("cannot convert nil to float64")
}
-
+
switch val := v.(type) {
case float64:
return val, nil
@@ -228,4 +228,4 @@ func toFloat64(v interface{}) (float64, error) {
default:
return 0, fmt.Errorf("cannot convert %T to float64", v)
}
-}
\ No newline at end of file
+}
diff --git a/examples/development_mode/main.go b/examples/development_mode/main.go
index 96da31c..29c7e7a 100644
--- a/examples/development_mode/main.go
+++ b/examples/development_mode/main.go
@@ -45,18 +45,18 @@ func main() {
// Create a Twig engine in development mode
engine := twig.New()
-
+
// Enable development mode
engine.SetDevelopmentMode(true)
fmt.Println("Development mode enabled:")
fmt.Printf(" - Cache enabled: %v\n", engine.IsCacheEnabled())
fmt.Printf(" - Debug enabled: %v\n", engine.IsDebugEnabled())
fmt.Printf(" - AutoReload: %v\n", engine.IsAutoReloadEnabled())
-
+
// Register a template loader
loader := twig.NewFileSystemLoader([]string{templatesDir})
engine.RegisterLoader(loader)
-
+
// Context for rendering
context := map[string]interface{}{
"title": "Twig Development Mode Example",
@@ -64,7 +64,7 @@ func main() {
"mode": "development",
"items": []string{"Easy to use", "No template caching", "Auto-reloading enabled"},
}
-
+
// Render the template to stdout
fmt.Println("\n--- Rendering in Development Mode ---")
err = engine.RenderTo(os.Stdout, "hello.twig", context)
@@ -72,21 +72,21 @@ func main() {
fmt.Printf("Error rendering template: %v\n", err)
return
}
-
+
// Cached templates should be empty in development mode
fmt.Printf("\n\nNumber of cached templates: %d\n", engine.GetCachedTemplateCount())
-
+
// Now switch to production mode
engine.SetDevelopmentMode(false)
fmt.Println("\nProduction mode enabled:")
fmt.Printf(" - Cache enabled: %v\n", engine.IsCacheEnabled())
fmt.Printf(" - Debug enabled: %v\n", engine.IsDebugEnabled())
fmt.Printf(" - AutoReload: %v\n", engine.IsAutoReloadEnabled())
-
+
// Change the context
context["mode"] = "production"
context["items"] = []string{"Maximum performance", "Template caching", "Optimized for speed"}
-
+
// Render again in production mode
fmt.Println("\n--- Rendering in Production Mode ---")
err = engine.RenderTo(os.Stdout, "hello.twig", context)
@@ -94,16 +94,16 @@ func main() {
fmt.Printf("Error rendering template: %v\n", err)
return
}
-
+
// Now we should have templates in the cache
fmt.Printf("\n\nNumber of cached templates: %d\n", engine.GetCachedTemplateCount())
fmt.Println("Template names in cache:")
for _, name := range engine.GetCachedTemplateNames() {
fmt.Printf(" - %s\n", name)
}
-
+
fmt.Println("\nDemonstrating auto-reload with file modification:")
-
+
// Let's modify the template file
fmt.Println("Modifying template file...")
modifiedContent := `
@@ -134,15 +134,15 @@ func main() {
fmt.Printf("Error modifying template: %v\n", err)
return
}
-
+
// Enable development mode
engine.SetDevelopmentMode(true)
context["title"] = "Twig Auto-Reload Example"
-
+
// Pause to make sure the file system registers the change
fmt.Println("Waiting for file system to register change...")
time.Sleep(1 * time.Second)
-
+
// Render the template again - it should load the modified version
fmt.Println("\n--- Rendering with Auto-Reload ---")
err = engine.RenderTo(os.Stdout, "hello.twig", context)
@@ -150,6 +150,6 @@ func main() {
fmt.Printf("Error rendering template: %v\n", err)
return
}
-
+
fmt.Println("\n\nSuccess! The template was automatically reloaded.")
-}
\ No newline at end of file
+}
diff --git a/examples/macros/main.go b/examples/macros/main.go
index 101550c..fdca3b9 100644
--- a/examples/macros/main.go
+++ b/examples/macros/main.go
@@ -74,4 +74,4 @@ func main() {
fmt.Printf("Error rendering template: %v\n", err)
return
}
-}
\ No newline at end of file
+}
diff --git a/examples/simple/main.go b/examples/simple/main.go
index 31c1aa0..c5c3d4d 100644
--- a/examples/simple/main.go
+++ b/examples/simple/main.go
@@ -59,26 +59,26 @@ func main() {
fmt.Printf("Error registering template: %v\n", err)
return
}
-
+
// Create a context with some products
context := map[string]interface{}{
"shop_name": "Twig Marketplace",
"products": []map[string]interface{}{
{
- "name": "Laptop",
+ "name": "Laptop",
"price": 1200,
},
{
- "name": "Phone",
+ "name": "Phone",
"price": 800,
},
{
- "name": "Headphones",
+ "name": "Headphones",
"price": 200,
},
},
}
-
+
// Render the template
fmt.Println("Rendering complex shop template with set tags:")
err = engine.RenderTo(os.Stdout, "shop_template", context)
@@ -86,4 +86,4 @@ func main() {
fmt.Printf("Error rendering template: %v\n", err)
return
}
-}
\ No newline at end of file
+}
diff --git a/expr.go b/expr.go
index 5da0cf4..48999a4 100644
--- a/expr.go
+++ b/expr.go
@@ -136,7 +136,7 @@ func (n *ExpressionNode) Line() int {
// Render implementation for LiteralNode
func (n *LiteralNode) Render(w io.Writer, ctx *RenderContext) error {
var str string
-
+
switch v := n.value.(type) {
case string:
str = v
@@ -151,7 +151,7 @@ func (n *LiteralNode) Render(w io.Writer, ctx *RenderContext) error {
default:
str = ctx.ToString(v)
}
-
+
_, err := w.Write([]byte(str))
return err
}
@@ -209,7 +209,7 @@ func (n *VariableNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(value)
_, err = w.Write([]byte(str))
return err
@@ -221,22 +221,22 @@ func (n *GetAttrNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
attrName, err := ctx.EvaluateExpression(n.attribute)
if err != nil {
return err
}
-
+
attrStr, ok := attrName.(string)
if !ok {
return fmt.Errorf("attribute name must be a string")
}
-
+
value, err := ctx.getAttribute(obj, attrStr)
if err != nil {
return err
}
-
+
str := ctx.ToString(value)
_, err = w.Write([]byte(str))
return err
@@ -248,7 +248,7 @@ func (n *BinaryNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -260,7 +260,7 @@ func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -272,7 +272,7 @@ func (n *TestNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -284,7 +284,7 @@ func (n *UnaryNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -296,7 +296,7 @@ func (n *ConditionalNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -308,7 +308,7 @@ func (n *ArrayNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
str := ctx.ToString(result)
_, err = w.Write([]byte(str))
return err
@@ -374,4 +374,4 @@ func NewArrayNode(items []Node, line int) *ArrayNode {
},
items: items,
}
-}
\ No newline at end of file
+}
diff --git a/extension.go b/extension.go
index aae6049..a4b279b 100644
--- a/extension.go
+++ b/extension.go
@@ -33,22 +33,22 @@ type OperatorFunc func(left, right interface{}) (interface{}, error)
type Extension interface {
// GetName returns the name of the extension
GetName() string
-
+
// GetFilters returns the filters defined by this extension
GetFilters() map[string]FilterFunc
-
+
// GetFunctions returns the functions defined by this extension
GetFunctions() map[string]FunctionFunc
-
+
// GetTests returns the tests defined by this extension
GetTests() map[string]TestFunc
-
+
// GetOperators returns the operators defined by this extension
GetOperators() map[string]OperatorFunc
-
+
// GetTokenParsers returns any custom token parsers
GetTokenParsers() []TokenParser
-
+
// Initialize initializes the extension
Initialize(*Engine)
}
@@ -57,7 +57,7 @@ type Extension interface {
type TokenParser interface {
// GetTag returns the tag this parser handles
GetTag() string
-
+
// Parse parses the tag and returns a node
Parse(*Parser, *Token) (Node, error)
}
@@ -73,51 +73,51 @@ func (e *CoreExtension) GetName() string {
// GetFilters returns the core filters
func (e *CoreExtension) GetFilters() map[string]FilterFunc {
return map[string]FilterFunc{
- "default": e.filterDefault,
- "escape": e.filterEscape,
- "e": e.filterEscape, // alias for escape
- "upper": e.filterUpper,
- "lower": e.filterLower,
- "trim": e.filterTrim,
- "raw": e.filterRaw,
- "length": e.filterLength,
- "count": e.filterLength, // alias for length
- "join": e.filterJoin,
- "split": e.filterSplit,
- "date": e.filterDate,
- "url_encode": e.filterUrlEncode,
- "capitalize": e.filterCapitalize,
- "first": e.filterFirst,
- "last": e.filterLast,
- "slice": e.filterSlice,
- "reverse": e.filterReverse,
- "sort": e.filterSort,
- "keys": e.filterKeys,
- "merge": e.filterMerge,
- "replace": e.filterReplace,
- "striptags": e.filterStripTags,
+ "default": e.filterDefault,
+ "escape": e.filterEscape,
+ "e": e.filterEscape, // alias for escape
+ "upper": e.filterUpper,
+ "lower": e.filterLower,
+ "trim": e.filterTrim,
+ "raw": e.filterRaw,
+ "length": e.filterLength,
+ "count": e.filterLength, // alias for length
+ "join": e.filterJoin,
+ "split": e.filterSplit,
+ "date": e.filterDate,
+ "url_encode": e.filterUrlEncode,
+ "capitalize": e.filterCapitalize,
+ "first": e.filterFirst,
+ "last": e.filterLast,
+ "slice": e.filterSlice,
+ "reverse": e.filterReverse,
+ "sort": e.filterSort,
+ "keys": e.filterKeys,
+ "merge": e.filterMerge,
+ "replace": e.filterReplace,
+ "striptags": e.filterStripTags,
"number_format": e.filterNumberFormat,
- "abs": e.filterAbs,
- "round": e.filterRound,
- "nl2br": e.filterNl2Br,
+ "abs": e.filterAbs,
+ "round": e.filterRound,
+ "nl2br": e.filterNl2Br,
}
}
// GetFunctions returns the core functions
func (e *CoreExtension) GetFunctions() map[string]FunctionFunc {
return map[string]FunctionFunc{
- "range": e.functionRange,
- "date": e.functionDate,
- "random": e.functionRandom,
- "max": e.functionMax,
- "min": e.functionMin,
- "dump": e.functionDump,
- "constant": e.functionConstant,
- "cycle": e.functionCycle,
- "include": e.functionInclude,
+ "range": e.functionRange,
+ "date": e.functionDate,
+ "random": e.functionRandom,
+ "max": e.functionMax,
+ "min": e.functionMin,
+ "dump": e.functionDump,
+ "constant": e.functionConstant,
+ "cycle": e.functionCycle,
+ "include": e.functionInclude,
"json_encode": e.functionJsonEncode,
- "length": e.functionLength,
- "merge": e.functionMerge,
+ "length": e.functionLength,
+ "merge": e.functionMerge,
}
}
@@ -145,13 +145,13 @@ func (e *CoreExtension) GetTests() map[string]TestFunc {
// GetOperators returns the core operators
func (e *CoreExtension) GetOperators() map[string]OperatorFunc {
return map[string]OperatorFunc{
- "in": e.operatorIn,
- "not in": e.operatorNotIn,
- "is": e.operatorIs,
- "is not": e.operatorIsNot,
- "matches": e.operatorMatches,
+ "in": e.operatorIn,
+ "not in": e.operatorNotIn,
+ "is": e.operatorIs,
+ "is not": e.operatorIsNot,
+ "matches": e.operatorMatches,
"starts with": e.operatorStartsWith,
- "ends with": e.operatorEndsWith,
+ "ends with": e.operatorEndsWith,
}
}
@@ -269,7 +269,7 @@ func (e *CoreExtension) filterJoin(value interface{}, args ...interface{}) (inte
delimiter = d
}
}
-
+
return join(value, delimiter)
}
@@ -280,7 +280,7 @@ func (e *CoreExtension) filterSplit(value interface{}, args ...interface{}) (int
delimiter = d
}
}
-
+
s := toString(value)
return strings.Split(s, delimiter), nil
}
@@ -288,7 +288,7 @@ func (e *CoreExtension) filterSplit(value interface{}, args ...interface{}) (int
func (e *CoreExtension) filterDate(value interface{}, args ...interface{}) (interface{}, error) {
// Get the datetime value
var dt time.Time
-
+
switch v := value.(type) {
case time.Time:
dt = v
@@ -311,14 +311,14 @@ func (e *CoreExtension) filterDate(value interface{}, args ...interface{}) (inte
"01/02/2006",
"01/02/2006 15:04:05",
}
-
+
for _, format := range formats {
dt, err = time.Parse(format, v)
if err == nil {
break
}
}
-
+
if err != nil {
return "", fmt.Errorf("cannot parse date from string: %s", v)
}
@@ -332,7 +332,7 @@ func (e *CoreExtension) filterDate(value interface{}, args ...interface{}) (inte
default:
return "", fmt.Errorf("cannot format date from type %T", value)
}
-
+
// Check for format string
format := "2006-01-02 15:04:05"
if len(args) > 0 {
@@ -341,7 +341,7 @@ func (e *CoreExtension) filterDate(value interface{}, args ...interface{}) (inte
format = convertDateFormat(f)
}
}
-
+
return dt.Format(format), nil
}
@@ -356,17 +356,17 @@ func (e *CoreExtension) functionRange(args ...interface{}) (interface{}, error)
if len(args) < 2 {
return nil, errors.New("range function requires at least 2 arguments")
}
-
+
start, err := toInt(args[0])
if err != nil {
return nil, err
}
-
+
end, err := toInt(args[1])
if err != nil {
return nil, err
}
-
+
step := 1
if len(args) > 2 {
s, err := toInt(args[2])
@@ -375,11 +375,11 @@ func (e *CoreExtension) functionRange(args ...interface{}) (interface{}, error)
}
step = s
}
-
+
if step == 0 {
return nil, errors.New("step cannot be zero")
}
-
+
var result []int
if step > 0 {
for i := start; i <= end; i += step {
@@ -390,14 +390,14 @@ func (e *CoreExtension) functionRange(args ...interface{}) (interface{}, error)
result = append(result, i)
}
}
-
+
return result, nil
}
func (e *CoreExtension) functionDate(args ...interface{}) (interface{}, error) {
// Default to current time
dt := time.Now()
-
+
// Check if a timestamp or date string was provided
if len(args) > 0 && args[0] != nil {
switch v := args[0].(type) {
@@ -426,14 +426,14 @@ func (e *CoreExtension) functionDate(args ...interface{}) (interface{}, error) {
"01/02/2006",
"01/02/2006 15:04:05",
}
-
+
for _, format := range formats {
dt, err = time.Parse(format, v)
if err == nil {
break
}
}
-
+
if err != nil {
return nil, fmt.Errorf("cannot parse date from string: %s", v)
}
@@ -447,7 +447,7 @@ func (e *CoreExtension) functionDate(args ...interface{}) (interface{}, error) {
dt = time.Unix(int64(v), 0)
}
}
-
+
// If a timezone is specified as second argument
if len(args) > 1 {
if tzName, ok := args[1].(string); ok {
@@ -457,48 +457,48 @@ func (e *CoreExtension) functionDate(args ...interface{}) (interface{}, error) {
}
}
}
-
+
return dt, nil
}
func (e *CoreExtension) functionRandom(args ...interface{}) (interface{}, error) {
// Seed the random number generator if not already seeded
rand.Seed(time.Now().UnixNano())
-
+
// No args - return a random number between 0 and 2147483647 (PHP's RAND_MAX)
if len(args) == 0 {
return rand.Int31(), nil
}
-
+
// One argument - return 0 through max-1
if len(args) == 1 {
max, err := toInt(args[0])
if err != nil {
return nil, err
}
-
+
if max <= 0 {
return nil, errors.New("max must be greater than 0")
}
-
+
return rand.Intn(max), nil
}
-
+
// Two arguments - min and max
min, err := toInt(args[0])
if err != nil {
return nil, err
}
-
+
max, err := toInt(args[1])
if err != nil {
return nil, err
}
-
+
if max <= min {
return nil, errors.New("max must be greater than min")
}
-
+
// Generate a random number in the range [min, max]
return min + rand.Intn(max-min+1), nil
}
@@ -507,22 +507,22 @@ func (e *CoreExtension) functionMax(args ...interface{}) (interface{}, error) {
if len(args) == 0 {
return nil, errors.New("max function requires at least one argument")
}
-
+
var max float64
var initialized bool
-
+
for i, arg := range args {
num, err := toFloat64(arg)
if err != nil {
return nil, fmt.Errorf("argument %d is not a number", i)
}
-
+
if !initialized || num > max {
max = num
initialized = true
}
}
-
+
return max, nil
}
@@ -530,22 +530,22 @@ func (e *CoreExtension) functionMin(args ...interface{}) (interface{}, error) {
if len(args) == 0 {
return nil, errors.New("min function requires at least one argument")
}
-
+
var min float64
var initialized bool
-
+
for i, arg := range args {
num, err := toFloat64(arg)
if err != nil {
return nil, fmt.Errorf("argument %d is not a number", i)
}
-
+
if !initialized || num < min {
min = num
initialized = true
}
}
-
+
return min, nil
}
@@ -553,7 +553,7 @@ func (e *CoreExtension) functionDump(args ...interface{}) (interface{}, error) {
if len(args) == 0 {
return "", nil
}
-
+
var result strings.Builder
for i, arg := range args {
if i > 0 {
@@ -561,7 +561,7 @@ func (e *CoreExtension) functionDump(args ...interface{}) (interface{}, error) {
}
result.WriteString(fmt.Sprintf("%#v", arg))
}
-
+
return result.String(), nil
}
@@ -615,21 +615,21 @@ func (e *CoreExtension) testDivisibleBy(value interface{}, args ...interface{})
if len(args) == 0 {
return false, errors.New("divisible_by test requires a divisor argument")
}
-
+
dividend, err := toInt(value)
if err != nil {
return false, err
}
-
+
divisor, err := toInt(args[0])
if err != nil {
return false, err
}
-
+
if divisor == 0 {
return false, errors.New("division by zero")
}
-
+
return dividend%divisor == 0, nil
}
@@ -642,14 +642,14 @@ func (e *CoreExtension) testEqualTo(value interface{}, args ...interface{}) (boo
if len(args) == 0 {
return false, errors.New("equalto test requires an argument")
}
-
+
// Get the comparison value
compareWith := args[0]
-
+
// Convert to strings and compare
str1 := toString(value)
str2 := toString(compareWith)
-
+
return str1 == str2, nil
}
@@ -657,11 +657,11 @@ func (e *CoreExtension) testStartsWith(value interface{}, args ...interface{}) (
if len(args) == 0 {
return false, errors.New("starts_with test requires a prefix argument")
}
-
+
// Convert to strings
str := toString(value)
prefix := toString(args[0])
-
+
return strings.HasPrefix(str, prefix), nil
}
@@ -669,11 +669,11 @@ func (e *CoreExtension) testEndsWith(value interface{}, args ...interface{}) (bo
if len(args) == 0 {
return false, errors.New("ends_with test requires a suffix argument")
}
-
+
// Convert to strings
str := toString(value)
suffix := toString(args[0])
-
+
return strings.HasSuffix(str, suffix), nil
}
@@ -681,17 +681,17 @@ func (e *CoreExtension) testMatches(value interface{}, args ...interface{}) (boo
if len(args) == 0 {
return false, errors.New("matches test requires a pattern argument")
}
-
+
// Convert to strings
str := toString(value)
pattern := toString(args[0])
-
+
// Compile the regex
regex, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid regular expression: %s", err)
}
-
+
return regex.MatchString(str), nil
}
@@ -701,7 +701,7 @@ func (e *CoreExtension) operatorIn(left, right interface{}) (interface{}, error)
if !isIterable(right) {
return false, errors.New("right operand must be iterable")
}
-
+
return contains(right, left)
}
@@ -709,12 +709,12 @@ func (e *CoreExtension) operatorNotIn(left, right interface{}) (interface{}, err
if !isIterable(right) {
return false, errors.New("right operand must be iterable")
}
-
+
result, err := contains(right, left)
if err != nil {
return false, err
}
-
+
return !result, nil
}
@@ -730,7 +730,7 @@ func (e *CoreExtension) operatorIsNot(left, right interface{}) (interface{}, err
if err != nil {
return false, err
}
-
+
return !(equal.(bool)), nil
}
@@ -738,13 +738,13 @@ func (e *CoreExtension) operatorMatches(left, right interface{}) (interface{}, e
// Convert to strings
str := toString(left)
pattern := toString(right)
-
+
// Compile the regex
regex, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid regular expression: %s", err)
}
-
+
return regex.MatchString(str), nil
}
@@ -752,7 +752,7 @@ func (e *CoreExtension) operatorStartsWith(left, right interface{}) (interface{}
// Convert to strings
str := toString(left)
prefix := toString(right)
-
+
return strings.HasPrefix(str, prefix), nil
}
@@ -760,7 +760,7 @@ func (e *CoreExtension) operatorEndsWith(left, right interface{}) (interface{},
// Convert to strings
str := toString(left)
suffix := toString(right)
-
+
return strings.HasSuffix(str, suffix), nil
}
@@ -770,7 +770,7 @@ func isEmptyValue(v interface{}) bool {
if v == nil {
return true
}
-
+
switch value := v.(type) {
case string:
return value == ""
@@ -787,7 +787,7 @@ func isEmptyValue(v interface{}) bool {
case map[string]interface{}:
return len(value) == 0
}
-
+
// Use reflection for other types
rv := reflect.ValueOf(v)
switch rv.Kind() {
@@ -804,7 +804,7 @@ func isEmptyValue(v interface{}) bool {
case reflect.String:
return rv.String() == ""
}
-
+
// Default behavior for other types
return false
}
@@ -813,19 +813,19 @@ func isIterable(v interface{}) bool {
if v == nil {
return false
}
-
+
switch v.(type) {
case string, []interface{}, map[string]interface{}:
return true
}
-
+
// Use reflection for other types
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return true
}
-
+
return false
}
@@ -833,7 +833,7 @@ func length(v interface{}) (int, error) {
if v == nil {
return 0, nil
}
-
+
switch value := v.(type) {
case string:
return len(value), nil
@@ -842,24 +842,24 @@ func length(v interface{}) (int, error) {
case map[string]interface{}:
return len(value), nil
}
-
+
// Use reflection for other types
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len(), nil
}
-
+
return 0, fmt.Errorf("cannot get length of %T", v)
}
func join(v interface{}, delimiter string) (string, error) {
var items []string
-
+
if v == nil {
return "", nil
}
-
+
// Handle different types
switch value := v.(type) {
case []string:
@@ -880,7 +880,7 @@ func join(v interface{}, delimiter string) (string, error) {
return toString(v), nil
}
}
-
+
return strings.Join(items, delimiter), nil
}
@@ -888,9 +888,9 @@ func contains(container, item interface{}) (bool, error) {
if container == nil {
return false, nil
}
-
+
itemStr := toString(item)
-
+
// Handle different container types
switch c := container.(type) {
case string:
@@ -927,7 +927,7 @@ func contains(container, item interface{}) (bool, error) {
}
}
}
-
+
return false, nil
}
@@ -935,7 +935,7 @@ func toString(v interface{}) string {
if v == nil {
return ""
}
-
+
switch val := v.(type) {
case string:
return val
@@ -952,7 +952,7 @@ func toString(v interface{}) string {
case fmt.Stringer:
return val.String()
}
-
+
return fmt.Sprintf("%v", v)
}
@@ -960,7 +960,7 @@ func toInt(v interface{}) (int, error) {
if v == nil {
return 0, errors.New("cannot convert nil to int")
}
-
+
switch val := v.(type) {
case int:
return val, nil
@@ -980,7 +980,7 @@ func toInt(v interface{}) (int, error) {
}
return 0, nil
}
-
+
return 0, fmt.Errorf("cannot convert %T to int", v)
}
@@ -988,7 +988,7 @@ func toFloat64(v interface{}) (float64, error) {
if v == nil {
return 0, errors.New("cannot convert nil to float64")
}
-
+
switch val := v.(type) {
case float64:
return val, nil
@@ -1010,7 +1010,7 @@ func toFloat64(v interface{}) (float64, error) {
}
return 0, nil
}
-
+
return 0, fmt.Errorf("cannot convert %T to float64", v)
}
@@ -1018,37 +1018,37 @@ func toFloat64(v interface{}) (float64, error) {
func convertDateFormat(format string) string {
replacements := map[string]string{
// Day
- "d": "02", // Day of the month, 2 digits with leading zeros
- "D": "Mon", // A textual representation of a day, three letters
- "j": "2", // Day of the month without leading zeros
- "l": "Monday", // A full textual representation of the day of the week
-
+ "d": "02", // Day of the month, 2 digits with leading zeros
+ "D": "Mon", // A textual representation of a day, three letters
+ "j": "2", // Day of the month without leading zeros
+ "l": "Monday", // A full textual representation of the day of the week
+
// Month
- "F": "January", // A full textual representation of a month
- "m": "01", // Numeric representation of a month, with leading zeros
- "M": "Jan", // A short textual representation of a month, three letters
- "n": "1", // Numeric representation of a month, without leading zeros
-
+ "F": "January", // A full textual representation of a month
+ "m": "01", // Numeric representation of a month, with leading zeros
+ "M": "Jan", // A short textual representation of a month, three letters
+ "n": "1", // Numeric representation of a month, without leading zeros
+
// Year
- "Y": "2006", // A full numeric representation of a year, 4 digits
- "y": "06", // A two digit representation of a year
-
+ "Y": "2006", // A full numeric representation of a year, 4 digits
+ "y": "06", // A two digit representation of a year
+
// Time
- "a": "pm", // Lowercase Ante meridiem and Post meridiem
- "A": "PM", // Uppercase Ante meridiem and Post meridiem
- "g": "3", // 12-hour format of an hour without leading zeros
- "G": "15", // 24-hour format of an hour without leading zeros
- "h": "03", // 12-hour format of an hour with leading zeros
- "H": "15", // 24-hour format of an hour with leading zeros
- "i": "04", // Minutes with leading zeros
- "s": "05", // Seconds with leading zeros
+ "a": "pm", // Lowercase Ante meridiem and Post meridiem
+ "A": "PM", // Uppercase Ante meridiem and Post meridiem
+ "g": "3", // 12-hour format of an hour without leading zeros
+ "G": "15", // 24-hour format of an hour without leading zeros
+ "h": "03", // 12-hour format of an hour with leading zeros
+ "H": "15", // 24-hour format of an hour with leading zeros
+ "i": "04", // Minutes with leading zeros
+ "s": "05", // Seconds with leading zeros
}
-
+
result := format
for phpFormat, goFormat := range replacements {
result = strings.ReplaceAll(result, phpFormat, goFormat)
}
-
+
return result
}
@@ -1059,14 +1059,14 @@ func (e *CoreExtension) filterCapitalize(value interface{}, args ...interface{})
if s == "" {
return "", nil
}
-
+
words := strings.Fields(s)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(word[0:1]) + strings.ToLower(word[1:])
}
}
-
+
return strings.Join(words, " "), nil
}
@@ -1074,7 +1074,7 @@ func (e *CoreExtension) filterFirst(value interface{}, args ...interface{}) (int
if value == nil {
return nil, nil
}
-
+
switch v := value.(type) {
case string:
if len(v) > 0 {
@@ -1092,7 +1092,7 @@ func (e *CoreExtension) filterFirst(value interface{}, args ...interface{}) (int
}
return nil, nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
switch rv.Kind() {
@@ -1113,7 +1113,7 @@ func (e *CoreExtension) filterFirst(value interface{}, args ...interface{}) (int
}
return nil, nil
}
-
+
return nil, fmt.Errorf("cannot get first element of %T", value)
}
@@ -1121,7 +1121,7 @@ func (e *CoreExtension) filterLast(value interface{}, args ...interface{}) (inte
if value == nil {
return nil, nil
}
-
+
switch v := value.(type) {
case string:
if len(v) > 0 {
@@ -1134,7 +1134,7 @@ func (e *CoreExtension) filterLast(value interface{}, args ...interface{}) (inte
}
return nil, nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
switch rv.Kind() {
@@ -1146,11 +1146,11 @@ func (e *CoreExtension) filterLast(value interface{}, args ...interface{}) (inte
return "", nil
case reflect.Array, reflect.Slice:
if rv.Len() > 0 {
- return rv.Index(rv.Len()-1).Interface(), nil
+ return rv.Index(rv.Len() - 1).Interface(), nil
}
return nil, nil
}
-
+
return nil, fmt.Errorf("cannot get last element of %T", value)
}
@@ -1158,7 +1158,7 @@ func (e *CoreExtension) filterReverse(value interface{}, args ...interface{}) (i
if value == nil {
return nil, nil
}
-
+
switch v := value.(type) {
case string:
// Reverse string
@@ -1175,7 +1175,7 @@ func (e *CoreExtension) filterReverse(value interface{}, args ...interface{}) (i
}
return result, nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
switch rv.Kind() {
@@ -1194,7 +1194,7 @@ func (e *CoreExtension) filterReverse(value interface{}, args ...interface{}) (i
}
return resultSlice.Interface(), nil
}
-
+
return nil, fmt.Errorf("cannot reverse %T", value)
}
@@ -1202,17 +1202,17 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
if value == nil {
return nil, nil
}
-
+
// Need at least the start index
if len(args) < 1 {
return nil, errors.New("slice filter requires at least one argument (start index)")
}
-
+
start, err := toInt(args[0])
if err != nil {
return nil, err
}
-
+
// Default length is to the end
length := -1
if len(args) > 1 {
@@ -1224,17 +1224,17 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
}
}
}
-
+
switch v := value.(type) {
case string:
runes := []rune(v)
runeCount := len(runes)
-
+
// Handle negative start index
if start < 0 {
start = runeCount + start
}
-
+
// Check bounds
if start < 0 {
start = 0
@@ -1242,7 +1242,7 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
if start >= runeCount {
return "", nil
}
-
+
// Calculate end index
end := runeCount
if length >= 0 {
@@ -1251,16 +1251,16 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
end = runeCount
}
}
-
+
return string(runes[start:end]), nil
case []interface{}:
count := len(v)
-
+
// Handle negative start index
if start < 0 {
start = count + start
}
-
+
// Check bounds
if start < 0 {
start = 0
@@ -1268,7 +1268,7 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
if start >= count {
return []interface{}{}, nil
}
-
+
// Calculate end index
end := count
if length >= 0 {
@@ -1277,10 +1277,10 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
end = count
}
}
-
+
return v[start:end], nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
switch rv.Kind() {
@@ -1288,12 +1288,12 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
s := rv.String()
runes := []rune(s)
runeCount := len(runes)
-
+
// Handle negative start index
if start < 0 {
start = runeCount + start
}
-
+
// Check bounds
if start < 0 {
start = 0
@@ -1301,7 +1301,7 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
if start >= runeCount {
return "", nil
}
-
+
// Calculate end index
end := runeCount
if length >= 0 {
@@ -1310,16 +1310,16 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
end = runeCount
}
}
-
+
return string(runes[start:end]), nil
case reflect.Array, reflect.Slice:
count := rv.Len()
-
+
// Handle negative start index
if start < 0 {
start = count + start
}
-
+
// Check bounds
if start < 0 {
start = 0
@@ -1327,7 +1327,7 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
if start >= count {
return reflect.MakeSlice(rv.Type(), 0, 0).Interface(), nil
}
-
+
// Calculate end index
end := count
if length >= 0 {
@@ -1336,16 +1336,16 @@ func (e *CoreExtension) filterSlice(value interface{}, args ...interface{}) (int
end = count
}
}
-
+
// Create a new slice with the same type
result := reflect.MakeSlice(rv.Type(), end-start, end-start)
for i := start; i < end; i++ {
- result.Index(i-start).Set(rv.Index(i))
+ result.Index(i - start).Set(rv.Index(i))
}
-
+
return result.Interface(), nil
}
-
+
return nil, fmt.Errorf("cannot slice %T", value)
}
@@ -1353,7 +1353,7 @@ func (e *CoreExtension) filterKeys(value interface{}, args ...interface{}) (inte
if value == nil {
return nil, nil
}
-
+
switch v := value.(type) {
case map[string]interface{}:
keys := make([]string, 0, len(v))
@@ -1362,7 +1362,7 @@ func (e *CoreExtension) filterKeys(value interface{}, args ...interface{}) (inte
}
return keys, nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
if rv.Kind() == reflect.Map {
@@ -1372,7 +1372,7 @@ func (e *CoreExtension) filterKeys(value interface{}, args ...interface{}) (inte
}
return keys, nil
}
-
+
return nil, fmt.Errorf("cannot get keys from %T, expected map", value)
}
@@ -1381,46 +1381,46 @@ func (e *CoreExtension) filterMerge(value interface{}, args ...interface{}) (int
rv := reflect.ValueOf(value)
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
result := reflect.MakeSlice(rv.Type(), rv.Len(), rv.Len())
-
+
// Copy original values
for i := 0; i < rv.Len(); i++ {
result.Index(i).Set(rv.Index(i))
}
-
+
// Add values from the arguments
for _, arg := range args {
argRv := reflect.ValueOf(arg)
if argRv.Kind() == reflect.Slice || argRv.Kind() == reflect.Array {
// Create a new slice with expanded capacity
newResult := reflect.MakeSlice(rv.Type(), result.Len()+argRv.Len(), result.Len()+argRv.Len())
-
+
// Copy existing values
for i := 0; i < result.Len(); i++ {
newResult.Index(i).Set(result.Index(i))
}
-
+
// Append the new values
for i := 0; i < argRv.Len(); i++ {
- newResult.Index(result.Len()+i).Set(argRv.Index(i))
+ newResult.Index(result.Len() + i).Set(argRv.Index(i))
}
-
+
result = newResult
}
}
-
+
return result.Interface(), nil
}
-
+
// Handle merging maps
if rv.Kind() == reflect.Map {
// Create a new map with the same key and value types
resultMap := reflect.MakeMap(rv.Type())
-
+
// Copy original values
for _, key := range rv.MapKeys() {
resultMap.SetMapIndex(key, rv.MapIndex(key))
}
-
+
// Merge values from the arguments
for _, arg := range args {
argRv := reflect.ValueOf(arg)
@@ -1430,30 +1430,30 @@ func (e *CoreExtension) filterMerge(value interface{}, args ...interface{}) (int
}
}
}
-
+
return resultMap.Interface(), nil
}
-
+
return value, nil
}
func (e *CoreExtension) filterReplace(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
-
+
if len(args) < 2 {
return s, errors.New("replace filter requires at least 2 arguments (search and replace values)")
}
-
+
// Get search and replace values
search := toString(args[0])
replace := toString(args[1])
-
+
return strings.ReplaceAll(s, search, replace), nil
}
func (e *CoreExtension) filterStripTags(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
-
+
// Very simple regexp-based HTML tag removal
re := regexp.MustCompile("<[^>]*>")
return re.ReplaceAllString(s, ""), nil
@@ -1463,7 +1463,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
if value == nil {
return nil, nil
}
-
+
switch v := value.(type) {
case []string:
result := make([]string, len(v))
@@ -1485,7 +1485,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
if len(v) == 0 {
return v, nil
}
-
+
// Check if all elements are strings
allStrings := true
for _, item := range v {
@@ -1494,7 +1494,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
break
}
}
-
+
if allStrings {
// Sort as strings
result := make([]interface{}, len(v))
@@ -1504,7 +1504,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
})
return result, nil
}
-
+
// Check if all elements are numbers
allNumbers := true
for _, item := range v {
@@ -1514,7 +1514,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
break
}
}
-
+
if allNumbers {
// Sort as numbers
result := make([]interface{}, len(v))
@@ -1529,7 +1529,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
})
return result, nil
}
-
+
// General sort using string representation
result := make([]interface{}, len(v))
copy(result, v)
@@ -1538,7 +1538,7 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
})
return result, nil
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(value)
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
@@ -1546,26 +1546,26 @@ func (e *CoreExtension) filterSort(value interface{}, args ...interface{}) (inte
for i := 0; i < rv.Len(); i++ {
result.Index(i).Set(rv.Index(i))
}
-
+
// Use sort.SliceStable for a stable sort
sort.SliceStable(result.Interface(), func(i, j int) bool {
a := result.Index(i).Interface()
b := result.Index(j).Interface()
-
+
// Try numeric comparison
aNum, err1 := toFloat64(a)
bNum, err2 := toFloat64(b)
if err1 == nil && err2 == nil {
return aNum < bNum
}
-
+
// Fall back to string comparison
return toString(a) < toString(b)
})
-
+
return result.Interface(), nil
}
-
+
return nil, fmt.Errorf("cannot sort %T", value)
}
@@ -1574,39 +1574,39 @@ func (e *CoreExtension) filterNumberFormat(value interface{}, args ...interface{
if err != nil {
return value, nil
}
-
+
// Default parameters
decimals := 0
decPoint := "."
thousandsSep := ","
-
+
// Parse parameters
if len(args) > 0 {
if d, err := toInt(args[0]); err == nil {
decimals = d
}
}
-
+
if len(args) > 1 {
if d, ok := args[1].(string); ok {
decPoint = d
}
}
-
+
if len(args) > 2 {
if t, ok := args[2].(string); ok {
thousandsSep = t
}
}
-
+
// Format the number
format := "%." + strconv.Itoa(decimals) + "f"
str := fmt.Sprintf(format, num)
-
+
// Split into integer and fractional parts
parts := strings.Split(str, ".")
intPart := parts[0]
-
+
// Add thousands separator
if thousandsSep != "" {
// Insert thousands separator
@@ -1619,7 +1619,7 @@ func (e *CoreExtension) filterNumberFormat(value interface{}, args ...interface{
}
intPart = buf.String()
}
-
+
// Add decimal point and fractional part if needed
if decimals > 0 {
if len(parts) > 1 {
@@ -1630,7 +1630,7 @@ func (e *CoreExtension) filterNumberFormat(value interface{}, args ...interface{
return intPart + decPoint + zeros, nil
}
}
-
+
return intPart, nil
}
@@ -1639,7 +1639,7 @@ func (e *CoreExtension) filterAbs(value interface{}, args ...interface{}) (inter
if err != nil {
return value, nil
}
-
+
return math.Abs(num), nil
}
@@ -1648,24 +1648,24 @@ func (e *CoreExtension) filterRound(value interface{}, args ...interface{}) (int
if err != nil {
return value, nil
}
-
+
precision := 0
method := "common"
-
+
// Parse precision argument
if len(args) > 0 {
if p, err := toInt(args[0]); err == nil {
precision = p
}
}
-
+
// Parse rounding method argument
if len(args) > 1 {
if m, ok := args[1].(string); ok {
method = strings.ToLower(m)
}
}
-
+
// Apply rounding
var result float64
switch method {
@@ -1679,23 +1679,23 @@ func (e *CoreExtension) filterRound(value interface{}, args ...interface{}) (int
shift := math.Pow(10, float64(precision))
result = math.Round(num*shift) / shift
}
-
+
// If precision is 0, return an integer
if precision == 0 {
return int(result), nil
}
-
+
return result, nil
}
func (e *CoreExtension) filterNl2Br(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
-
+
// Replace newlines with
s = strings.ReplaceAll(s, "\r\n", "
")
s = strings.ReplaceAll(s, "\n", "
")
s = strings.ReplaceAll(s, "\r", "
")
-
+
return s, nil
}
@@ -1705,10 +1705,10 @@ func (e *CoreExtension) functionCycle(args ...interface{}) (interface{}, error)
if len(args) < 2 {
return nil, errors.New("cycle function requires at least two arguments (values to cycle through and position)")
}
-
+
position := 0
var values []interface{}
-
+
// Last argument is the position if it's a number
lastArg := args[len(args)-1]
if pos, err := toInt(lastArg); err == nil {
@@ -1719,18 +1719,18 @@ func (e *CoreExtension) functionCycle(args ...interface{}) (interface{}, error)
values = args
// Position defaults to 0
}
-
+
// Handle empty values
if len(values) == 0 {
return nil, nil
}
-
+
// Get the value at the specified position (with wrapping)
index := position % len(values)
if index < 0 {
index += len(values)
}
-
+
return values[index], nil
}
@@ -1743,25 +1743,25 @@ func (e *CoreExtension) functionJsonEncode(args ...interface{}) (interface{}, er
if len(args) == 0 {
return "null", nil
}
-
+
// Default options
options := 0
-
+
// Check for options flag
if len(args) > 1 {
if opt, err := toInt(args[1]); err == nil {
options = opt
}
}
-
+
// Convert the value to JSON
data, err := json.Marshal(args[0])
if err != nil {
return "", err
}
-
+
result := string(data)
-
+
// Apply options (simplified)
// In real Twig, there are constants like JSON_PRETTY_PRINT, JSON_HEX_TAG, etc.
// Here we just do a simple pretty print if options is non-zero
@@ -1771,7 +1771,7 @@ func (e *CoreExtension) functionJsonEncode(args ...interface{}) (interface{}, er
result = prettyJSON.String()
}
}
-
+
return result, nil
}
@@ -1779,7 +1779,7 @@ func (e *CoreExtension) functionLength(args ...interface{}) (interface{}, error)
if len(args) != 1 {
return nil, errors.New("length function requires exactly one argument")
}
-
+
return length(args[0])
}
@@ -1787,22 +1787,22 @@ func (e *CoreExtension) functionMerge(args ...interface{}) (interface{}, error)
if len(args) < 2 {
return nil, errors.New("merge function requires at least two arguments to merge")
}
-
+
// Get the first argument as the base value
base := args[0]
-
+
// If it's an array or slice, merge with other arrays
rv := reflect.ValueOf(base)
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
// Start with a copy of the base slice
var result []interface{}
-
+
// Add base elements
baseRv := reflect.ValueOf(base)
for i := 0; i < baseRv.Len(); i++ {
result = append(result, baseRv.Index(i).Interface())
}
-
+
// Add elements from the other slices
for i := 1; i < len(args); i++ {
arg := args[i]
@@ -1816,15 +1816,15 @@ func (e *CoreExtension) functionMerge(args ...interface{}) (interface{}, error)
result = append(result, arg)
}
}
-
+
return result, nil
}
-
+
// If it's a map, merge with other maps
if rv.Kind() == reflect.Map {
// Create a new map to store the merged result
result := make(map[string]interface{})
-
+
// Add all entries from the base map
if baseMap, ok := base.(map[string]interface{}); ok {
for k, v := range baseMap {
@@ -1838,7 +1838,7 @@ func (e *CoreExtension) functionMerge(args ...interface{}) (interface{}, error)
result[keyStr] = baseRv.MapIndex(key).Interface()
}
}
-
+
// Add entries from other maps
for i := 1; i < len(args); i++ {
arg := args[i]
@@ -1857,13 +1857,13 @@ func (e *CoreExtension) functionMerge(args ...interface{}) (interface{}, error)
}
}
}
-
+
return result, nil
}
-
+
return nil, fmt.Errorf("cannot merge %T, expected array or map", base)
}
func escapeHTML(s string) string {
return html.EscapeString(s)
-}
\ No newline at end of file
+}
diff --git a/extension_test.go b/extension_test.go
index 6440276..c05bc70 100644
--- a/extension_test.go
+++ b/extension_test.go
@@ -8,33 +8,33 @@ import (
// TestAddFilter tests the AddFilter function
func TestAddFilter(t *testing.T) {
engine := New()
-
+
// Add a custom filter
engine.AddFilter("reverse_words", func(value interface{}, args ...interface{}) (interface{}, error) {
s := toString(value)
words := strings.Fields(s)
-
+
// Reverse the order of words
for i, j := 0, len(words)-1; i < j; i, j = i+1, j-1 {
words[i], words[j] = words[j], words[i]
}
-
+
return strings.Join(words, " "), nil
})
-
+
// Create a test template using the custom filter
source := "{{ 'hello world'|reverse_words }}"
template, err := engine.ParseTemplate(source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
// Render the template
result, err := template.Render(nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
-
+
expected := "world hello"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
@@ -44,35 +44,35 @@ func TestAddFilter(t *testing.T) {
// TestAddFunction tests the AddFunction function
func TestAddFunction(t *testing.T) {
engine := New()
-
+
// Add a custom function
engine.AddFunction("repeat", func(args ...interface{}) (interface{}, error) {
if len(args) < 2 {
return "", nil
}
-
+
text := toString(args[0])
count, err := toInt(args[1])
if err != nil {
return "", err
}
-
+
return strings.Repeat(text, count), nil
})
-
+
// Create a test template using the custom function
source := "{{ repeat('abc', 3) }}"
template, err := engine.ParseTemplate(source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
// Render the template
result, err := template.Render(nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
-
+
expected := "abcabcabc"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
@@ -82,7 +82,7 @@ func TestAddFunction(t *testing.T) {
// TestCustomExtension tests the custom extension functionality
func TestCustomExtension(t *testing.T) {
engine := New()
-
+
// Create and register a custom extension
engine.RegisterExtension("test_extension", func(ext *CustomExtension) {
// Add a filter
@@ -95,54 +95,54 @@ func TestCustomExtension(t *testing.T) {
}
return string(runes), nil
}
-
+
// Add a function
ext.Functions["add"] = func(args ...interface{}) (interface{}, error) {
if len(args) < 2 {
return 0, nil
}
-
+
a, errA := toFloat64(args[0])
b, errB := toFloat64(args[1])
-
+
if errA != nil || errB != nil {
return 0, nil
}
-
+
return a + b, nil
}
})
-
+
// Test the filter
filterSource := "{{ 'hello'|shuffle }}"
filterTemplate, err := engine.ParseTemplate(filterSource)
if err != nil {
t.Fatalf("Error parsing filter template: %v", err)
}
-
+
filterResult, err := filterTemplate.Render(nil)
if err != nil {
t.Fatalf("Error rendering filter template: %v", err)
}
-
+
// Due to how our shuffle works, we know the exact result
filterExpected := "olleh"
if filterResult != filterExpected {
t.Errorf("Expected filter result to be %q, but got %q", filterExpected, filterResult)
}
-
+
// Test the function
funcSource := "{{ add(2, 3) }}"
funcTemplate, err := engine.ParseTemplate(funcSource)
if err != nil {
t.Fatalf("Error parsing function template: %v", err)
}
-
+
funcResult, err := funcTemplate.Render(nil)
if err != nil {
t.Fatalf("Error rendering function template: %v", err)
}
-
+
funcExpected := "5"
if funcResult != funcExpected {
t.Errorf("Expected function result to be %q, but got %q", funcExpected, funcResult)
@@ -152,7 +152,7 @@ func TestCustomExtension(t *testing.T) {
// TestMultipleExtensions tests registering multiple extensions
func TestMultipleExtensions(t *testing.T) {
engine := New()
-
+
// Register first extension
engine.RegisterExtension("first_extension", func(ext *CustomExtension) {
ext.Filters["double"] = func(value interface{}, args ...interface{}) (interface{}, error) {
@@ -163,7 +163,7 @@ func TestMultipleExtensions(t *testing.T) {
return num * 2, nil
}
})
-
+
// Register second extension
engine.RegisterExtension("second_extension", func(ext *CustomExtension) {
ext.Filters["triple"] = func(value interface{}, args ...interface{}) (interface{}, error) {
@@ -174,22 +174,22 @@ func TestMultipleExtensions(t *testing.T) {
return num * 3, nil
}
})
-
+
// Test using both extensions in a single template
source := "{{ 5|double|triple }}"
template, err := engine.ParseTemplate(source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
result, err := template.Render(nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
-
+
// 5 doubled is 10, then tripled is 30
expected := "30"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
}
-}
\ No newline at end of file
+}
diff --git a/gen/lexer.gen.go b/gen/lexer.gen.go
index 9e216ec..48979d3 100644
--- a/gen/lexer.gen.go
+++ b/gen/lexer.gen.go
@@ -39,12 +39,12 @@ func (l *Lexer) Tokenize() ([]Token, error) {
l.line++
l.col = 1
l.pos++
-
+
case l.isWhitespace(l.source[l.pos]):
// Skip whitespace
l.col++
l.pos++
-
+
case l.match("{{"):
// Variable start
l.addToken(T_VAR_START, "{{")
@@ -52,12 +52,12 @@ func (l *Lexer) Tokenize() ([]Token, error) {
// After variable start, scan for identifiers, operators, etc.
l.scanExpressionTokens()
-
+
case l.match("}}"):
// Variable end
l.addToken(T_VAR_END, "}}")
l.advance(2)
-
+
case l.match("{%"):
// Block start
l.addToken(T_BLOCK_START, "{%")
@@ -65,17 +65,17 @@ func (l *Lexer) Tokenize() ([]Token, error) {
// After block start, scan for identifiers, operators, etc.
l.scanExpressionTokens()
-
+
case l.match("%}"):
// Block end
l.addToken(T_BLOCK_END, "%}")
l.advance(2)
-
+
default:
// Text content (anything that's not a special delimiter)
start := l.pos
- for l.pos < len(l.source) &&
- !l.match("{{") && !l.match("}}") &&
+ for l.pos < len(l.source) &&
+ !l.match("{{") && !l.match("}}") &&
!l.match("{%") && !l.match("%}") {
if l.source[l.pos] == '\n' {
l.line++
@@ -85,7 +85,7 @@ func (l *Lexer) Tokenize() ([]Token, error) {
}
l.pos++
}
-
+
if start != l.pos {
l.addToken(T_TEXT, l.source[start:l.pos])
} else {
@@ -98,7 +98,7 @@ func (l *Lexer) Tokenize() ([]Token, error) {
// Add EOF token
l.addToken(T_EOF, "")
-
+
return l.tokens, nil
}
@@ -110,9 +110,9 @@ func (l *Lexer) scanExpressionTokens() {
}
// Continue scanning until we reach the end tag
- for l.pos < len(l.source) &&
+ for l.pos < len(l.source) &&
!l.match("}}") && !l.match("%}") {
-
+
// Skip whitespace
if l.isWhitespace(l.source[l.pos]) {
l.advance(1)
@@ -152,18 +152,18 @@ func (l *Lexer) scanExpressionTokens() {
// scanIdentifierOrKeyword scans an identifier or keyword
func (l *Lexer) scanIdentifierOrKeyword() {
start := l.pos
-
+
// First character is already checked to be alpha
l.advance(1)
-
+
// Keep scanning alphanumeric and underscore characters
for l.pos < len(l.source) && (l.isAlphaNumeric(l.source[l.pos]) || l.source[l.pos] == '_') {
l.advance(1)
}
-
+
// Extract the identifier
text := l.source[start:l.pos]
-
+
// Check if it's a keyword
switch text {
case "macro":
@@ -194,24 +194,24 @@ func (l *Lexer) scanIdentifierOrKeyword() {
// scanNumber scans a number (integer or float)
func (l *Lexer) scanNumber() {
start := l.pos
-
+
// Keep scanning digits
for l.pos < len(l.source) && l.isDigit(l.source[l.pos]) {
l.advance(1)
}
-
+
// Look for fractional part
- if l.pos < len(l.source) && l.source[l.pos] == '.' &&
+ if l.pos < len(l.source) && l.source[l.pos] == '.' &&
l.pos+1 < len(l.source) && l.isDigit(l.source[l.pos+1]) {
// Consume the dot
l.advance(1)
-
+
// Consume digits after the dot
for l.pos < len(l.source) && l.isDigit(l.source[l.pos]) {
l.advance(1)
}
}
-
+
l.addToken(T_NUMBER, l.source[start:l.pos])
}
@@ -219,10 +219,10 @@ func (l *Lexer) scanNumber() {
func (l *Lexer) scanString() {
start := l.pos
quote := l.source[l.pos] // ' or "
-
+
// Consume the opening quote
l.advance(1)
-
+
// Keep scanning until closing quote or end of file
for l.pos < len(l.source) && l.source[l.pos] != quote {
// Handle escape sequence
@@ -232,12 +232,12 @@ func (l *Lexer) scanString() {
l.advance(1)
}
}
-
+
// Consume the closing quote
if l.pos < len(l.source) {
l.advance(1)
}
-
+
l.addToken(T_STRING, l.source[start:l.pos])
}
@@ -245,8 +245,8 @@ func (l *Lexer) scanString() {
func (l *Lexer) scanOperator() {
// Check for multi-character operators first
if l.pos+1 < len(l.source) {
- twoChars := l.source[l.pos:l.pos+2]
-
+ twoChars := l.source[l.pos : l.pos+2]
+
switch twoChars {
case "==", "!=", ">=", "<=", "&&", "||", "+=", "-=", "*=", "/=", "%=", "~=":
l.addToken(T_OPERATOR, twoChars)
@@ -254,7 +254,7 @@ func (l *Lexer) scanOperator() {
return
}
}
-
+
// Single character operators
l.addToken(T_OPERATOR, string(l.source[l.pos]))
l.advance(1)
@@ -275,13 +275,13 @@ func (l *Lexer) isAlphaNumeric(c byte) bool {
}
func (l *Lexer) isPunctuation(c byte) bool {
- return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' ||
- c == ',' || c == '.' || c == ':' || c == ';'
+ return c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' ||
+ c == ',' || c == '.' || c == ':' || c == ';'
}
func (l *Lexer) isOperator(c byte) bool {
- return c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '=' ||
- c == '<' || c == '>' || c == '!' || c == '&' || c == '|' || c == '~'
+ return c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || c == '=' ||
+ c == '<' || c == '>' || c == '!' || c == '&' || c == '|' || c == '~'
}
// Helper methods
@@ -309,4 +309,4 @@ func (l *Lexer) match(s string) bool {
func (l *Lexer) isWhitespace(c byte) bool {
return c == ' ' || c == '\t' || c == '\r'
-}
\ No newline at end of file
+}
diff --git a/gen/nodes.gen.go b/gen/nodes.gen.go
index f9523e6..1780a85 100644
--- a/gen/nodes.gen.go
+++ b/gen/nodes.gen.go
@@ -33,10 +33,10 @@ const (
type Node interface {
// Render renders the node to the given writer
Render(w io.Writer, ctx *RenderContext) error
-
+
// Type returns the type of the node
Type() NodeType
-
+
// Line returns the line number where the node was defined
Line() int
}
@@ -79,7 +79,7 @@ func (n *MacroNode) Call(w io.Writer, ctx *RenderContext, args []interface{}) er
parent: ctx.parent,
engine: ctx.engine,
}
-
+
// Set parameter values from arguments
for i, param := range n.params {
if i < len(args) {
@@ -97,14 +97,14 @@ func (n *MacroNode) Call(w io.Writer, ctx *RenderContext, args []interface{}) er
macroContext.context[param] = nil
}
}
-
+
// Render the macro body
for _, node := range n.body {
if err := node.Render(w, macroContext); err != nil {
return err
}
}
-
+
return nil
}
@@ -122,7 +122,7 @@ func (n *ImportNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
// Create a context for the imported template
importContext := &RenderContext{
env: ctx.env,
@@ -131,24 +131,24 @@ func (n *ImportNode) Render(w io.Writer, ctx *RenderContext) error {
macros: make(map[string]Node),
engine: ctx.engine,
}
-
+
// Render the template to collect macros (without output)
var nullWriter NullWriter
if err := template.nodes.Render(&nullWriter, importContext); err != nil {
return err
}
-
+
// Create a module object to hold the imported macros
module := make(map[string]interface{})
-
+
// Add all macros from the imported template to the module
for name, macro := range importContext.macros {
module[name] = macro
}
-
+
// Set the module in the current context
ctx.SetVariable(n.module, module)
-
+
return nil
}
@@ -175,7 +175,7 @@ func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
// Create a context for the imported template
importContext := &RenderContext{
env: ctx.env,
@@ -184,13 +184,13 @@ func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
macros: make(map[string]Node),
engine: ctx.engine,
}
-
+
// Render the template to collect macros (without output)
var nullWriter NullWriter
if err := template.nodes.Render(&nullWriter, importContext); err != nil {
return err
}
-
+
// Import the specified macros directly into the current context
if len(n.macros) > 0 {
for _, name := range n.macros {
@@ -199,7 +199,7 @@ func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
}
}
}
-
+
// Import macros with aliases
if len(n.aliases) > 0 {
for name, alias := range n.aliases {
@@ -208,7 +208,7 @@ func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
}
}
}
-
+
return nil
}
@@ -235,7 +235,7 @@ func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
if err != nil {
return err
}
-
+
// Evaluate filter arguments
var args []interface{}
for _, arg := range n.args {
@@ -245,13 +245,13 @@ func (n *FilterNode) Render(w io.Writer, ctx *RenderContext) error {
}
args = append(args, argValue)
}
-
+
// Apply the filter
result, err := ctx.ApplyFilter(n.filter, input, args)
if err != nil {
return err
}
-
+
// Write the result
_, err = w.Write([]byte(ctx.ToString(result)))
return err
@@ -314,4 +314,4 @@ func NewFilterNode(node Node, filter string, args []Node, line int) *FilterNode
args: args,
line: line,
}
-}
\ No newline at end of file
+}
diff --git a/gen/parser.gen.go b/gen/parser.gen.go
index 6c68c4c..49a15ab 100644
--- a/gen/parser.gen.go
+++ b/gen/parser.gen.go
@@ -9,8 +9,8 @@ import (
// Parser is responsible for parsing tokens into an abstract syntax tree
type Parser struct {
- tokens []Token
- tokenIndex int
+ tokens []Token
+ tokenIndex int
blockHandlers map[string]BlockHandlerFunc
}
@@ -20,8 +20,8 @@ type BlockHandlerFunc func(*Parser) (Node, error)
// NewParser creates a new parser for the given tokens
func NewParser() *Parser {
p := &Parser{
- tokens: nil,
- tokenIndex: 0,
+ tokens: nil,
+ tokenIndex: 0,
blockHandlers: make(map[string]BlockHandlerFunc),
}
p.initBlockHandlers()
@@ -35,10 +35,10 @@ func (p *Parser) Parse(source string) (Node, error) {
if err != nil {
return nil, fmt.Errorf("lexer error: %w", err)
}
-
+
p.tokens = tokens
p.tokenIndex = 0
-
+
return p.parseOuterTemplate()
}
@@ -55,101 +55,101 @@ func (p *Parser) initBlockHandlers() {
"macro": p.parseMacro,
"import": p.parseImport,
"from": p.parseFrom,
-
+
// Special closing tags - they will be handled in their corresponding open tag parsers
- "endif": p.parseEndTag,
- "endfor": p.parseEndTag,
+ "endif": p.parseEndTag,
+ "endfor": p.parseEndTag,
"endblock": p.parseEndTag,
- "else": p.parseEndTag,
- "elseif": p.parseEndTag,
+ "else": p.parseEndTag,
+ "elseif": p.parseEndTag,
}
}
// parseOuterTemplate parses the outer template structure
func (p *Parser) parseOuterTemplate() (Node, error) {
nodes := []Node{}
-
+
for p.tokenIndex < len(p.tokens) {
token := p.tokens[p.tokenIndex]
-
+
switch token.Type {
case T_EOF:
// End of file, exit loop
p.tokenIndex++
break
-
+
case T_TEXT:
// Text node
nodes = append(nodes, NewTextNode(token.Value, token.Line))
p.tokenIndex++
-
+
case T_VAR_START:
// Variable output
p.tokenIndex++ // Skip {{
-
+
// Parse expression
expr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Expect }}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_VAR_END {
- return nil, fmt.Errorf("unexpected token, expected }} at line %d",
+ return nil, fmt.Errorf("unexpected token, expected }} at line %d",
token.Line)
}
-
+
// Create print node
nodes = append(nodes, NewPrintNode(expr, token.Line))
p.tokenIndex++ // Skip }}
-
+
case T_BLOCK_START:
// Block tag
p.tokenIndex++ // Skip {%
-
+
// Get block name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
- return nil, fmt.Errorf("unexpected token after {%%, expected block name at line %d",
+ return nil, fmt.Errorf("unexpected token after {%%, expected block name at line %d",
token.Line)
}
-
+
blockName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Check if this is a control ending tag (endif, endfor, endblock, etc.)
- if blockName == "endif" || blockName == "endfor" || blockName == "endblock" ||
- blockName == "else" || blockName == "elseif" {
+ if blockName == "endif" || blockName == "endfor" || blockName == "endblock" ||
+ blockName == "else" || blockName == "elseif" {
// We should return to the parent parser that's handling the parent block
// First move back two steps to the start of the block tag
p.tokenIndex -= 2
return NewRootNode(nodes, 1), nil
}
-
+
// Check if we have a handler for this block type
handler, ok := p.blockHandlers[blockName]
if !ok {
- return nil, fmt.Errorf("unknown block tag '%s' at line %d",
+ return nil, fmt.Errorf("unknown block tag '%s' at line %d",
blockName, token.Line)
}
-
+
// Call the handler
node, err := handler(p)
if err != nil {
return nil, err
}
-
+
// Add the result to the nodes list
if node != nil {
nodes = append(nodes, node)
}
-
+
default:
// Unexpected token
- return nil, fmt.Errorf("unexpected token '%s' at line %d",
+ return nil, fmt.Errorf("unexpected token '%s' at line %d",
token.Value, token.Line)
}
}
-
+
return NewRootNode(nodes, 1), nil
}
@@ -163,28 +163,28 @@ func (p *Parser) parseEndTag(parser *Parser) (Node, error) {
func (p *Parser) parseExpression() (Node, error) {
// For now, implement a simple expression parser
// This will be expanded later to handle complex expressions
-
+
if p.tokenIndex >= len(p.tokens) {
return nil, fmt.Errorf("unexpected end of input while parsing expression")
}
-
+
token := p.tokens[p.tokenIndex]
-
+
switch token.Type {
case T_IDENT:
// Variable reference
p.tokenIndex++
return NewVariableNode(token.Value, token.Line), nil
-
+
case T_STRING:
// String literal
p.tokenIndex++
return NewLiteralNode(token.Value, token.Line), nil
-
+
case T_NUMBER:
// Number literal
p.tokenIndex++
-
+
// Parse number
if strings.Contains(token.Value, ".") {
// Float
@@ -195,9 +195,9 @@ func (p *Parser) parseExpression() (Node, error) {
val, _ := strconv.ParseInt(token.Value, 10, 64)
return NewLiteralNode(int(val), token.Line), nil
}
-
+
default:
- return nil, fmt.Errorf("unexpected token '%s' in expression at line %d",
+ return nil, fmt.Errorf("unexpected token '%s' in expression at line %d",
token.Value, token.Line)
}
}
@@ -209,20 +209,20 @@ func (p *Parser) parseIf(*Parser) (Node, error) {
if err != nil {
return nil, err
}
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
- return nil, fmt.Errorf("unexpected token, expected %%} at line %d",
+ return nil, fmt.Errorf("unexpected token, expected %%} at line %d",
p.tokens[p.tokenIndex-1].Line)
}
p.tokenIndex++ // Skip %}
-
+
// Parse if body
ifBody, err := p.parseOuterTemplate()
if err != nil {
return nil, err
}
-
+
// Get body nodes
var bodyNodes []Node
if rootNode, ok := ifBody.(*RootNode); ok {
@@ -230,32 +230,32 @@ func (p *Parser) parseIf(*Parser) (Node, error) {
} else {
bodyNodes = []Node{ifBody}
}
-
+
// Check for else
var elseNodes []Node
-
+
// Check if we're at an else tag
- if p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_BLOCK_START &&
- p.tokens[p.tokenIndex+1].Type == T_IDENT &&
- p.tokens[p.tokenIndex+1].Value == "else" {
-
+ if p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_BLOCK_START &&
+ p.tokens[p.tokenIndex+1].Type == T_IDENT &&
+ p.tokens[p.tokenIndex+1].Value == "else" {
+
// Skip {% else %}
p.tokenIndex += 2 // Skip {% else
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
- return nil, fmt.Errorf("unexpected token, expected %%} after else at line %d",
+ return nil, fmt.Errorf("unexpected token, expected %%} after else at line %d",
p.tokens[p.tokenIndex-1].Line)
}
p.tokenIndex++ // Skip %}
-
+
// Parse else body
elseBody, err := p.parseOuterTemplate()
if err != nil {
return nil, err
}
-
+
// Get else nodes
if rootNode, ok := elseBody.(*RootNode); ok {
elseNodes = rootNode.Children()
@@ -263,26 +263,26 @@ func (p *Parser) parseIf(*Parser) (Node, error) {
elseNodes = []Node{elseBody}
}
}
-
+
// Check for endif
- if p.tokenIndex+1 >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_BLOCK_START ||
- p.tokens[p.tokenIndex+1].Type != T_IDENT ||
- p.tokens[p.tokenIndex+1].Value != "endif" {
- return nil, fmt.Errorf("missing endif tag at line %d",
+ if p.tokenIndex+1 >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_BLOCK_START ||
+ p.tokens[p.tokenIndex+1].Type != T_IDENT ||
+ p.tokens[p.tokenIndex+1].Value != "endif" {
+ return nil, fmt.Errorf("missing endif tag at line %d",
p.tokens[p.tokenIndex].Line)
}
-
+
// Skip {% endif %}
p.tokenIndex += 2 // Skip {% endif
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
- return nil, fmt.Errorf("unexpected token, expected %%} after endif at line %d",
+ return nil, fmt.Errorf("unexpected token, expected %%} after endif at line %d",
p.tokens[p.tokenIndex-1].Line)
}
p.tokenIndex++ // Skip %}
-
+
// Create if node
return NewIfNode(condition, bodyNodes, elseNodes, p.tokens[p.tokenIndex-1].Line), nil
}
@@ -297,58 +297,58 @@ func (p *Parser) parseFor(*Parser) (Node, error) {
func (p *Parser) parseSet(*Parser) (Node, error) {
// Get the line number of the set token
setLine := p.tokens[p.tokenIndex-2].Line
-
+
// Get the variable name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected variable name after set at line %d", setLine)
}
-
+
varName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Expect '='
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_OPERATOR ||
- p.tokens[p.tokenIndex].Value != "=" {
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_OPERATOR ||
+ p.tokens[p.tokenIndex].Value != "=" {
return nil, fmt.Errorf("expected '=' after variable name at line %d", setLine)
}
p.tokenIndex++
-
+
// Parse the value expression
valueExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// For expressions like 5 + 10, we need to parse both sides and make a binary node
// Check if there's an operator after the first token
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_OPERATOR &&
- p.tokens[p.tokenIndex].Value != "=" {
-
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_OPERATOR &&
+ p.tokens[p.tokenIndex].Value != "=" {
+
// Get the operator
operator := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Parse the right side
rightExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Create a binary node
valueExpr = NewBinaryNode(operator, valueExpr, rightExpr, setLine)
}
-
+
// Expect the block end token
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
return nil, fmt.Errorf("expected block end token after set expression at line %d", setLine)
}
p.tokenIndex++
-
+
// Create the set node
setNode := NewSetNode(varName, valueExpr, setLine)
-
+
return setNode, nil
}
@@ -380,89 +380,89 @@ func (p *Parser) parseDo(*Parser) (Node, error) {
func (p *Parser) parseMacro(*Parser) (Node, error) {
// Get the line number of the macro token
macroLine := p.tokens[p.tokenIndex-2].Line
-
+
// Get the macro name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected macro name after macro keyword at line %d", macroLine)
}
-
+
macroName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Expect opening parenthesis for parameters
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
- p.tokens[p.tokenIndex].Value != "(" {
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
+ p.tokens[p.tokenIndex].Value != "(" {
return nil, fmt.Errorf("expected '(' after macro name at line %d", macroLine)
}
p.tokenIndex++
-
+
// Parse parameters
var params []string
defaults := make(map[string]Node)
-
+
// If we don't have a closing parenthesis immediately, we have parameters
- if p.tokenIndex < len(p.tokens) &&
- (p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
- p.tokens[p.tokenIndex].Value != ")") {
-
+ if p.tokenIndex < len(p.tokens) &&
+ (p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
+ p.tokens[p.tokenIndex].Value != ")") {
+
for {
// Get parameter name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected parameter name at line %d", macroLine)
}
-
+
paramName := p.tokens[p.tokenIndex].Value
params = append(params, paramName)
p.tokenIndex++
-
+
// Check for default value
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_OPERATOR &&
- p.tokens[p.tokenIndex].Value == "=" {
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_OPERATOR &&
+ p.tokens[p.tokenIndex].Value == "=" {
p.tokenIndex++ // Skip =
-
+
// Parse default value expression
defaultExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
defaults[paramName] = defaultExpr
}
-
+
// Check if we have more parameters
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == "," {
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
-
+
break
}
}
-
+
// Expect closing parenthesis
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
- p.tokens[p.tokenIndex].Value != ")" {
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_PUNCTUATION ||
+ p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected ')' after macro parameters at line %d", macroLine)
}
p.tokenIndex++
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
return nil, fmt.Errorf("expected block end token after macro declaration at line %d", macroLine)
}
p.tokenIndex++
-
+
// Parse the macro body
bodyNode, err := p.parseOuterTemplate()
if err != nil {
return nil, err
}
-
+
// Extract body nodes
var bodyNodes []Node
if rootNode, ok := bodyNode.(*RootNode); ok {
@@ -470,25 +470,25 @@ func (p *Parser) parseMacro(*Parser) (Node, error) {
} else {
bodyNodes = []Node{bodyNode}
}
-
+
// Expect endmacro tag
- if p.tokenIndex+1 >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_BLOCK_START ||
- p.tokens[p.tokenIndex+1].Type != T_IDENT ||
- p.tokens[p.tokenIndex+1].Value != "endmacro" {
- return nil, fmt.Errorf("missing endmacro tag for macro '%s' at line %d",
+ if p.tokenIndex+1 >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_BLOCK_START ||
+ p.tokens[p.tokenIndex+1].Type != T_IDENT ||
+ p.tokens[p.tokenIndex+1].Value != "endmacro" {
+ return nil, fmt.Errorf("missing endmacro tag for macro '%s' at line %d",
macroName, macroLine)
}
-
+
// Skip {% endmacro %}
p.tokenIndex += 2 // Skip {% endmacro
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
return nil, fmt.Errorf("expected block end token after endmacro at line %d", p.tokens[p.tokenIndex].Line)
}
p.tokenIndex++
-
+
// Create the macro node
return NewMacroNode(macroName, params, defaults, bodyNodes, macroLine), nil
}
@@ -497,35 +497,35 @@ func (p *Parser) parseMacro(*Parser) (Node, error) {
func (p *Parser) parseImport(*Parser) (Node, error) {
// Get the line number of the import token
importLine := p.tokens[p.tokenIndex-2].Line
-
+
// Get the template to import
templateExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Expect 'as' keyword
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_IDENT ||
- p.tokens[p.tokenIndex].Value != "as" {
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_IDENT ||
+ p.tokens[p.tokenIndex].Value != "as" {
return nil, fmt.Errorf("expected 'as' after template path at line %d", importLine)
}
p.tokenIndex++
-
+
// Get the alias name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected identifier after 'as' at line %d", importLine)
}
-
+
alias := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
return nil, fmt.Errorf("expected block end token after import statement at line %d", importLine)
}
p.tokenIndex++
-
+
// Create import node
return NewImportNode(templateExpr, alias, importLine), nil
}
@@ -534,46 +534,46 @@ func (p *Parser) parseImport(*Parser) (Node, error) {
func (p *Parser) parseFrom(*Parser) (Node, error) {
// Get the line number of the from token
fromLine := p.tokens[p.tokenIndex-2].Line
-
+
// Get the template to import from
templateExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Expect 'import' keyword
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != T_IDENT ||
- p.tokens[p.tokenIndex].Value != "import" {
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != T_IDENT ||
+ p.tokens[p.tokenIndex].Value != "import" {
return nil, fmt.Errorf("expected 'import' after template path at line %d", fromLine)
}
p.tokenIndex++
-
+
// Parse the imported items
var macros []string
aliases := make(map[string]string)
-
+
// We need at least one macro to import
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected at least one identifier after 'import' at line %d", fromLine)
}
-
+
for p.tokenIndex < len(p.tokens) && p.tokens[p.tokenIndex].Type == T_IDENT {
// Get macro name
macroName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Check for 'as' keyword for aliasing
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_IDENT &&
- p.tokens[p.tokenIndex].Value == "as" {
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_IDENT &&
+ p.tokens[p.tokenIndex].Value == "as" {
p.tokenIndex++ // Skip 'as'
-
+
// Get alias name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected identifier after 'as' at line %d", fromLine)
}
-
+
aliasName := p.tokens[p.tokenIndex].Value
aliases[macroName] = aliasName
p.tokenIndex++
@@ -581,13 +581,13 @@ func (p *Parser) parseFrom(*Parser) (Node, error) {
// No alias, just add to macros list
macros = append(macros, macroName)
}
-
+
// Check for comma to separate items
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == T_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == "," {
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == T_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
-
+
// Expect another identifier after comma
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_IDENT {
return nil, fmt.Errorf("expected identifier after ',' at line %d", fromLine)
@@ -597,13 +597,13 @@ func (p *Parser) parseFrom(*Parser) (Node, error) {
break
}
}
-
+
// Expect %}
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != T_BLOCK_END {
return nil, fmt.Errorf("expected block end token after from import statement at line %d", fromLine)
}
p.tokenIndex++
-
+
// Create from import node
return NewFromImportNode(templateExpr, macros, aliases, fromLine), nil
-}
\ No newline at end of file
+}
diff --git a/gen/tokens.gen.go b/gen/tokens.gen.go
index db18a5c..841c1ca 100644
--- a/gen/tokens.gen.go
+++ b/gen/tokens.gen.go
@@ -19,7 +19,7 @@ const (
T_NUMBER
T_OPERATOR
T_PUNCTUATION
-
+
// Special keywords
T_MACRO
T_ENDMACRO
@@ -90,4 +90,4 @@ func (t Token) String() string {
typeName = fmt.Sprintf("UNKNOWN(%d)", t.Type)
}
return fmt.Sprintf("%s(%q)", typeName, t.Value)
-}
\ No newline at end of file
+}
diff --git a/loader.go b/loader.go
index 04d5faa..ea6f6eb 100644
--- a/loader.go
+++ b/loader.go
@@ -10,7 +10,7 @@ import (
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
}
@@ -18,7 +18,7 @@ type Loader interface {
// 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)
}
@@ -46,22 +46,22 @@ type ChainLoader struct {
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,
+ paths: normalizedPaths,
+ suffix: ".twig",
+ defaultPaths: defaultPaths,
templatePaths: make(map[string]string),
}
}
@@ -77,7 +77,7 @@ func (l *FileSystemLoader) Load(name string) (string, error) {
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
@@ -87,27 +87,27 @@ func (l *FileSystemLoader) Load(name string) (string, error) {
// 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)
}
@@ -116,18 +116,18 @@ 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
}
@@ -148,29 +148,29 @@ func (l *FileSystemLoader) GetModifiedTime(name string) (int64, error) {
}
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)
}
@@ -186,7 +186,7 @@ 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)
}
@@ -215,7 +215,7 @@ func (l *ChainLoader) Load(name string) (string, error) {
return loader.Load(name)
}
}
-
+
return "", fmt.Errorf("%w: %s", ErrTemplateNotFound, name)
}
@@ -226,7 +226,7 @@ func (l *ChainLoader) Exists(name string) bool {
return true
}
}
-
+
return false
}
@@ -238,4 +238,4 @@ func (l *ChainLoader) AddLoader(loader 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
-}
\ No newline at end of file
+}
diff --git a/parser.go b/parser.go
index 1de20c7..bf4c733 100644
--- a/parser.go
+++ b/parser.go
@@ -8,25 +8,25 @@ import (
// Token types
const (
- TOKEN_TEXT = iota
- TOKEN_VAR_START // {{
- TOKEN_VAR_END // }}
- TOKEN_BLOCK_START // {%
- TOKEN_BLOCK_END // %}
- TOKEN_COMMENT_START // {#
- TOKEN_COMMENT_END // #}
+ TOKEN_TEXT = iota
+ TOKEN_VAR_START // {{
+ TOKEN_VAR_END // }}
+ TOKEN_BLOCK_START // {%
+ TOKEN_BLOCK_END // %}
+ TOKEN_COMMENT_START // {#
+ TOKEN_COMMENT_END // #}
TOKEN_NAME
TOKEN_NUMBER
TOKEN_STRING
TOKEN_OPERATOR
TOKEN_PUNCTUATION
TOKEN_EOF
-
+
// Whitespace control token types
- TOKEN_VAR_START_TRIM // {{-
- TOKEN_VAR_END_TRIM // -}}
- TOKEN_BLOCK_START_TRIM // {%-
- TOKEN_BLOCK_END_TRIM // -%}
+ TOKEN_VAR_START_TRIM // {{-
+ TOKEN_VAR_END_TRIM // -}}
+ TOKEN_BLOCK_START_TRIM // {%-
+ TOKEN_BLOCK_END_TRIM // -%}
)
// Parser handles parsing Twig templates into node trees
@@ -68,17 +68,16 @@ func (p *Parser) Parse(source string) (Node, error) {
// Debug tokenization output
/*
- fmt.Println("Tokenized template:")
- for i, t := range p.tokens {
- fmt.Printf("Token %d: Type=%d, Value=%q, Line=%d\n", i, t.Type, t.Value, t.Line)
- }
+ fmt.Println("Tokenized template:")
+ for i, t := range p.tokens {
+ fmt.Printf("Token %d: Type=%d, Value=%q, Line=%d\n", i, t.Type, t.Value, t.Line)
+ }
*/
// Apply whitespace control processing to the tokens to handle
// the whitespace trimming between template elements
p.tokens = processWhitespaceControl(p.tokens)
-
-
+
// Parse tokens into nodes
nodes, err := p.parseOuterTemplate()
if err != nil {
@@ -221,21 +220,21 @@ func (p *Parser) tokenize() ([]Token, error) {
if p.cursor+1 < len(p.source) && p.current() == '{' && (p.source[p.cursor+1] == '{' || p.source[p.cursor+1] == '%') {
inEmbeddedVar = true
}
-
+
// Check for end of embedded variable
if inEmbeddedVar && p.cursor+1 < len(p.source) && p.current() == '}' && (p.source[p.cursor+1] == '}' || p.source[p.cursor+1] == '%') {
- p.cursor += 2 // Skip the closing brackets
+ p.cursor += 2 // Skip the closing brackets
inEmbeddedVar = false
continue
}
-
+
// Skip escaped quote characters
if p.current() == '\\' && p.cursor+1 < len(p.source) {
// Skip the backslash and the next character (which might be a quote)
p.cursor += 2
continue
}
-
+
if p.current() == '\n' {
p.line++
}
@@ -311,9 +310,9 @@ func (p *Parser) tokenize() ([]Token, error) {
// Handle plain text
start := p.cursor
for p.cursor < len(p.source) &&
- !p.matchString("{{-") && !p.matchString("{{") &&
+ !p.matchString("{{-") && !p.matchString("{{") &&
!p.matchString("-}}") && !p.matchString("}}") &&
- !p.matchString("{%-") && !p.matchString("{%") &&
+ !p.matchString("{%-") && !p.matchString("{%") &&
!p.matchString("-%}") && !p.matchString("%}") &&
!p.matchString("{#") && !p.matchString("#}") {
if p.current() == '\n' {
@@ -421,25 +420,25 @@ func fixHTMLAttributes(input string) string {
if attrStart == -1 {
break // No more attributes with embedded variables
}
-
+
attrStart += i // Adjust to full string position
-
+
// Find the end of the attribute value
attrEnd := strings.Index(input[attrStart+3:], "}}\"")
if attrEnd == -1 {
break // No closing variable
}
-
+
attrEnd += attrStart + 3 // Adjust to full string position
-
+
// Extract the variable name (between {{ and }})
- varName := strings.TrimSpace(input[attrStart+3:attrEnd])
-
+ varName := strings.TrimSpace(input[attrStart+3 : attrEnd])
+
// Replace the attribute string with an empty string for now
// We'll need to handle this specially in the parsing logic
input = input[:attrStart] + "=" + varName + input[attrEnd+2:]
}
-
+
return input
}
@@ -531,28 +530,28 @@ func (p *Parser) parseOuterTemplate() ([]Node, error) {
// For raw names, punctuation, operators, and literals not inside tags, convert to text
// In many languages, the text "true" is a literal boolean, but in our parser it's just a name token
// outside of an expression context
-
+
// Special handling for text content words - add spaces between consecutive text tokens
// This fixes issues with the spaceless tag's handling of text content
- if token.Type == TOKEN_NAME && p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
- p.tokens[p.tokenIndex+1].Line == token.Line {
+ if token.Type == TOKEN_NAME && p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
+ p.tokens[p.tokenIndex+1].Line == token.Line {
// Look ahead for consecutive name tokens and join them with spaces
var textContent strings.Builder
textContent.WriteString(token.Value)
-
+
currentLine := token.Line
p.tokenIndex++ // Skip the first token as we've already added it
-
+
// Collect consecutive name tokens on the same line
- for p.tokenIndex < len(p.tokens) &&
+ for p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex].Line == currentLine {
textContent.WriteString(" ") // Add space between words
textContent.WriteString(p.tokens[p.tokenIndex].Value)
p.tokenIndex++
}
-
+
nodes = append(nodes, NewTextNode(textContent.String(), token.Line))
} else {
// Regular handling for single text tokens
@@ -577,10 +576,10 @@ func (p *Parser) parseExpression() (Node, error) {
}
// Now check for filter operator (|)
- if p.tokenIndex < len(p.tokens) &&
+ if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "|" {
-
+
expr, err = p.parseFilters(expr)
if err != nil {
return nil, err
@@ -588,62 +587,62 @@ func (p *Parser) parseExpression() (Node, error) {
}
// Check for binary operators (and, or, ==, !=, <, >, etc.)
- if p.tokenIndex < len(p.tokens) &&
- (p.tokens[p.tokenIndex].Type == TOKEN_OPERATOR ||
- (p.tokens[p.tokenIndex].Type == TOKEN_NAME &&
- (p.tokens[p.tokenIndex].Value == "and" ||
- p.tokens[p.tokenIndex].Value == "or" ||
- p.tokens[p.tokenIndex].Value == "in" ||
- p.tokens[p.tokenIndex].Value == "not" ||
- p.tokens[p.tokenIndex].Value == "is" ||
- p.tokens[p.tokenIndex].Value == "matches" ||
- p.tokens[p.tokenIndex].Value == "starts" ||
- p.tokens[p.tokenIndex].Value == "ends"))) {
-
+ if p.tokenIndex < len(p.tokens) &&
+ (p.tokens[p.tokenIndex].Type == TOKEN_OPERATOR ||
+ (p.tokens[p.tokenIndex].Type == TOKEN_NAME &&
+ (p.tokens[p.tokenIndex].Value == "and" ||
+ p.tokens[p.tokenIndex].Value == "or" ||
+ p.tokens[p.tokenIndex].Value == "in" ||
+ p.tokens[p.tokenIndex].Value == "not" ||
+ p.tokens[p.tokenIndex].Value == "is" ||
+ p.tokens[p.tokenIndex].Value == "matches" ||
+ p.tokens[p.tokenIndex].Value == "starts" ||
+ p.tokens[p.tokenIndex].Value == "ends"))) {
+
expr, err = p.parseBinaryExpression(expr)
if err != nil {
return nil, err
}
}
-
+
// Check for ternary operator (? :)
- if p.tokenIndex < len(p.tokens) &&
+ if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "?" {
-
+
return p.parseConditionalExpression(expr)
}
-
+
return expr, nil
}
// Parse ternary conditional expression (condition ? true_expr : false_expr)
func (p *Parser) parseConditionalExpression(condition Node) (Node, error) {
line := p.tokens[p.tokenIndex].Line
-
+
// Skip the "?" token
p.tokenIndex++
-
+
// Parse the "true" expression
trueExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Expect ":" token
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ":" {
return nil, fmt.Errorf("expected ':' after true expression in conditional at line %d", line)
}
p.tokenIndex++ // Skip ":"
-
+
// Parse the "false" expression
falseExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
// Create a conditional node
return &ConditionalNode{
ExpressionNode: ExpressionNode{
@@ -686,11 +685,11 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
case TOKEN_NAME:
p.tokenIndex++
-
+
// Store the variable name for function calls
varName := token.Value
varLine := token.Line
-
+
// Special handling for boolean literals and null
if varName == "true" {
return NewLiteralNode(true, varLine), nil
@@ -701,21 +700,21 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
}
// Check if this is a function call (name followed by opening parenthesis)
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
-
+
// This is a function call
p.tokenIndex++ // Skip the opening parenthesis
-
+
// Parse arguments list
var args []Node
-
+
// If there are arguments (not empty parentheses)
- if p.tokenIndex < len(p.tokens) &&
- !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == ")") {
-
+ if p.tokenIndex < len(p.tokens) &&
+ !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == ")") {
+
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
@@ -723,32 +722,32 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
return nil, err
}
args = append(args, argExpr)
-
+
// Check for comma separator between arguments
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
-
+
// No comma, so must be end of argument list
break
}
}
-
+
// Expect closing parenthesis
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after function arguments at line %d", varLine)
}
p.tokenIndex++ // Skip closing parenthesis
-
+
// Create and return function node
return NewFunctionNode(varName, args, varLine), nil
}
-
+
// If not a function call, it's a regular variable
var result Node = NewVariableNode(varName, varLine)
@@ -770,13 +769,13 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
}
return result, nil
-
+
case TOKEN_PUNCTUATION:
// Handle array literals [1, 2, 3]
if token.Value == "[" {
return p.parseArrayExpression()
}
-
+
// Handle parenthesized expressions
if token.Value == "(" {
p.tokenIndex++ // Skip "("
@@ -784,22 +783,22 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
if err != nil {
return nil, err
}
-
+
// Expect closing parenthesis
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis at line %d", token.Line)
}
p.tokenIndex++ // Skip ")"
-
+
return expr, nil
}
default:
return nil, fmt.Errorf("unexpected token in expression at line %d", token.Line)
}
-
+
return nil, fmt.Errorf("unexpected token in expression at line %d", token.Line)
}
@@ -807,18 +806,18 @@ func (p *Parser) parseSimpleExpression() (Node, error) {
func (p *Parser) parseArrayExpression() (Node, error) {
// Save the line number for error reporting
line := p.tokens[p.tokenIndex].Line
-
+
// Skip the opening bracket
p.tokenIndex++
-
+
// Parse the array items
var items []Node
-
+
// Check if there are any items
- if p.tokenIndex < len(p.tokens) &&
- !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == "]") {
-
+ if p.tokenIndex < len(p.tokens) &&
+ !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == "]") {
+
for {
// Parse each item expression
itemExpr, err := p.parseExpression()
@@ -826,28 +825,28 @@ func (p *Parser) parseArrayExpression() (Node, error) {
return nil, err
}
items = append(items, itemExpr)
-
+
// Check for comma separator between items
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
-
+
// No comma, so must be end of array
break
}
}
-
+
// Expect closing bracket
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != "]" {
return nil, fmt.Errorf("expected closing bracket after array items at line %d", line)
}
p.tokenIndex++ // Skip closing bracket
-
+
// Create array node
return &ArrayNode{
ExpressionNode: ExpressionNode{
@@ -863,35 +862,35 @@ func (p *Parser) parseFilters(node Node) (Node, error) {
line := p.tokens[p.tokenIndex].Line
// Loop to handle multiple filters (e.g. var|filter1|filter2)
- for p.tokenIndex < len(p.tokens) &&
+ for p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "|" {
-
+
p.tokenIndex++ // Skip the | token
-
+
// Expect filter name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected filter name at line %d", line)
}
-
+
filterName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
-
+
// Check for filter arguments
var args []Node
-
+
// If there are arguments in parentheses
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
-
+
p.tokenIndex++ // Skip opening parenthesis
-
+
// Parse arguments
- if p.tokenIndex < len(p.tokens) &&
- !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == ")") {
-
+ if p.tokenIndex < len(p.tokens) &&
+ !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == ")") {
+
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
@@ -899,29 +898,29 @@ func (p *Parser) parseFilters(node Node) (Node, error) {
return nil, err
}
args = append(args, argExpr)
-
+
// Check for comma separator
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
-
+
// No comma, so end of argument list
break
}
}
-
+
// Expect closing parenthesis
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after filter arguments at line %d", line)
}
p.tokenIndex++ // Skip closing parenthesis
}
-
+
// Create a new FilterNode
node = &FilterNode{
ExpressionNode: ExpressionNode{
@@ -933,7 +932,7 @@ func (p *Parser) parseFilters(node Node) (Node, error) {
args: args,
}
}
-
+
return node, nil
}
@@ -942,29 +941,29 @@ func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
token := p.tokens[p.tokenIndex]
operator := token.Value
line := token.Line
-
+
// Process multi-word operators
if token.Type == TOKEN_NAME {
// Handle 'not in' operator
- if token.Value == "not" && p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
+ if token.Value == "not" && p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "in" {
operator = "not in"
p.tokenIndex += 2 // Skip both 'not' and 'in'
- } else if token.Value == "is" && p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
+ } else if token.Value == "is" && p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "not" {
// Handle 'is not' operator
operator = "is not"
p.tokenIndex += 2 // Skip both 'is' and 'not'
- } else if token.Value == "starts" && p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
+ } else if token.Value == "starts" && p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "with" {
// Handle 'starts with' operator
operator = "starts with"
p.tokenIndex += 2 // Skip both 'starts' and 'with'
- } else if token.Value == "ends" && p.tokenIndex+1 < len(p.tokens) &&
- p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
+ } else if token.Value == "ends" && p.tokenIndex+1 < len(p.tokens) &&
+ p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "with" {
// Handle 'ends with' operator
operator = "ends with"
@@ -977,29 +976,29 @@ func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
// Regular operators like +, -, *, /, etc.
p.tokenIndex++ // Skip the operator token
}
-
+
// Handle 'is' followed by a test
if operator == "is" || operator == "is not" {
// Check if this is a test
if p.tokenIndex < len(p.tokens) && p.tokens[p.tokenIndex].Type == TOKEN_NAME {
testName := p.tokens[p.tokenIndex].Value
p.tokenIndex++ // Skip the test name
-
+
// Parse test arguments if any
var args []Node
-
+
// If there's an opening parenthesis, parse arguments
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
-
+
p.tokenIndex++ // Skip opening parenthesis
-
+
// Parse arguments
- if p.tokenIndex < len(p.tokens) &&
- !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
- p.tokens[p.tokenIndex].Value == ")") {
-
+ if p.tokenIndex < len(p.tokens) &&
+ !(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ p.tokens[p.tokenIndex].Value == ")") {
+
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
@@ -1007,29 +1006,29 @@ func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
return nil, err
}
args = append(args, argExpr)
-
+
// Check for comma separator
- if p.tokenIndex < len(p.tokens) &&
- p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
+ if p.tokenIndex < len(p.tokens) &&
+ p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
-
+
// No comma, so end of argument list
break
}
}
-
+
// Expect closing parenthesis
- if p.tokenIndex >= len(p.tokens) ||
- p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
+ if p.tokenIndex >= len(p.tokens) ||
+ p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after test arguments at line %d", line)
}
p.tokenIndex++ // Skip closing parenthesis
}
-
+
// Create the test node
test := &TestNode{
ExpressionNode: ExpressionNode{
@@ -1040,7 +1039,7 @@ func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
test: testName,
args: args,
}
-
+
// If it's a negated test (is not), create a unary 'not' node
if operator == "is not" {
return &UnaryNode{
@@ -1052,19 +1051,19 @@ func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
node: test,
}, nil
}
-
+
return test, nil
}
}
-
+
// If we get here, we have a regular binary operator
-
+
// For regular binary operators, parse the right operand
right, err := p.parseExpression()
if err != nil {
return nil, err
}
-
+
return NewBinaryNode(operator, left, right, line), nil
}
@@ -1080,9 +1079,9 @@ func (p *Parser) parseIf(parser *Parser) (Node, error) {
}
// Expect the block end token (either regular or whitespace-trimming variant)
- if parser.tokenIndex >= len(parser.tokens) ||
- (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
- parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
+ if parser.tokenIndex >= len(parser.tokens) ||
+ (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
+ parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end after if condition at line %d", ifLine)
}
parser.tokenIndex++
@@ -1774,42 +1773,42 @@ func (p *Parser) parseEndTag(parser *Parser) (Node, error) {
func (p *Parser) parseSpaceless(parser *Parser) (Node, error) {
// Get the line number of the spaceless token
spacelessLine := parser.tokens[parser.tokenIndex-2].Line
-
+
// Expect the block end token
- if parser.tokenIndex >= len(parser.tokens) ||
- (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
- parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
+ if parser.tokenIndex >= len(parser.tokens) ||
+ (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
+ parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after spaceless at line %d", spacelessLine)
}
parser.tokenIndex++
-
+
// Parse the spaceless body
spacelessBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
-
+
// Expect endspaceless tag
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("expected endspaceless tag at line %d", spacelessLine)
}
parser.tokenIndex++
-
+
// Expect the endspaceless token
- if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
- parser.tokens[parser.tokenIndex].Value != "endspaceless" {
+ if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
+ parser.tokens[parser.tokenIndex].Value != "endspaceless" {
return nil, fmt.Errorf("expected endspaceless token at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
-
+
// Expect the block end token
- if parser.tokenIndex >= len(parser.tokens) ||
- (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
- parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
+ if parser.tokenIndex >= len(parser.tokens) ||
+ (parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
+ parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after endspaceless at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
-
+
// Create and return the spaceless node
return NewSpacelessNode(spacelessBody, spacelessLine), nil
}
diff --git a/render.go b/render.go
index 28d57f4..bd5b3a9 100644
--- a/render.go
+++ b/render.go
@@ -18,9 +18,9 @@ type RenderContext struct {
blocks map[string][]Node
macros map[string]Node
parent *RenderContext
- engine *Engine // Reference to engine for loading templates
- extending bool // Whether this template extends another
- currentBlock *BlockNode // Current block being rendered (for parent() function)
+ engine *Engine // Reference to engine for loading templates
+ extending bool // Whether this template extends another
+ currentBlock *BlockNode // Current block being rendered (for parent() function)
}
// Error types
@@ -32,26 +32,25 @@ var (
ErrRender = errors.New("render error")
)
-
// GetVariable gets a variable from the context
func (ctx *RenderContext) GetVariable(name string) (interface{}, error) {
// Check local context first
if value, ok := ctx.context[name]; ok {
return value, nil
}
-
+
// Check globals
if ctx.env != nil {
if value, ok := ctx.env.globals[name]; ok {
return value, nil
}
}
-
+
// Check parent context
if ctx.parent != nil {
return ctx.parent.GetVariable(name)
}
-
+
// Return nil with no error for undefined variables
// Twig treats undefined variables as empty strings during rendering
return nil, nil
@@ -68,12 +67,12 @@ func (ctx *RenderContext) GetMacro(name string) (Node, bool) {
if macro, ok := ctx.macros[name]; ok {
return macro, true
}
-
+
// Check parent context
if ctx.parent != nil {
return ctx.parent.GetMacro(name)
}
-
+
return nil, false
}
@@ -84,13 +83,13 @@ func (ctx *RenderContext) CallMacro(w io.Writer, name string, args []interface{}
if !ok {
return fmt.Errorf("macro '%s' not found", name)
}
-
+
// Check if it's a MacroNode
macroNode, ok := macro.(*MacroNode)
if !ok {
return fmt.Errorf("'%s' is not a macro", name)
}
-
+
// Call the macro
return macroNode.Call(w, ctx, args)
}
@@ -103,7 +102,7 @@ func (ctx *RenderContext) CallFunction(name string, args []interface{}) (interfa
return fn(args...)
}
}
-
+
// Check if it's a built-in function
switch name {
case "range":
@@ -115,7 +114,7 @@ func (ctx *RenderContext) CallFunction(name string, args []interface{}) (interfa
case "min":
return ctx.callMinFunction(args)
}
-
+
// Check if it's a macro
if macro, ok := ctx.GetMacro(name); ok {
// Return a callable function
@@ -127,7 +126,7 @@ func (ctx *RenderContext) CallFunction(name string, args []interface{}) (interfa
return macroNode.Call(w, ctx, args)
}, nil
}
-
+
return nil, fmt.Errorf("function '%s' not found", name)
}
@@ -136,15 +135,15 @@ func (ctx *RenderContext) callRangeFunction(args []interface{}) (interface{}, er
if len(args) < 2 {
return nil, fmt.Errorf("range function requires at least 2 arguments")
}
-
+
// Get the start and end values
start, ok1 := ctx.toNumber(args[0])
end, ok2 := ctx.toNumber(args[1])
-
+
if !ok1 || !ok2 {
return nil, fmt.Errorf("range arguments must be numbers")
}
-
+
// Get the step value (default is 1)
step := 1.0
if len(args) > 2 {
@@ -152,13 +151,13 @@ func (ctx *RenderContext) callRangeFunction(args []interface{}) (interface{}, er
step = s
}
}
-
+
// Create the range
var result []int
for i := start; i <= end; i += step {
result = append(result, int(i))
}
-
+
return result, nil
}
@@ -167,10 +166,10 @@ func (ctx *RenderContext) callLengthFunction(args []interface{}) (interface{}, e
if len(args) != 1 {
return nil, fmt.Errorf("length/count function requires exactly 1 argument")
}
-
+
val := args[0]
v := reflect.ValueOf(val)
-
+
switch v.Kind() {
case reflect.String:
return len(v.String()), nil
@@ -188,7 +187,7 @@ func (ctx *RenderContext) callMaxFunction(args []interface{}) (interface{}, erro
if len(args) < 1 {
return nil, fmt.Errorf("max function requires at least 1 argument")
}
-
+
// If the argument is a slice or array, find the max value in it
if len(args) == 1 {
v := reflect.ValueOf(args[0])
@@ -196,13 +195,13 @@ func (ctx *RenderContext) callMaxFunction(args []interface{}) (interface{}, erro
if v.Len() == 0 {
return nil, nil
}
-
+
max := v.Index(0).Interface()
maxNum, ok := ctx.toNumber(max)
if !ok {
return max, nil
}
-
+
for i := 1; i < v.Len(); i++ {
val := v.Index(i).Interface()
if valNum, ok := ctx.toNumber(val); ok {
@@ -212,18 +211,18 @@ func (ctx *RenderContext) callMaxFunction(args []interface{}) (interface{}, erro
}
}
}
-
+
return max, nil
}
}
-
+
// Find the max value in the arguments
max := args[0]
maxNum, ok := ctx.toNumber(max)
if !ok {
return max, nil
}
-
+
for i := 1; i < len(args); i++ {
val := args[i]
if valNum, ok := ctx.toNumber(val); ok {
@@ -233,7 +232,7 @@ func (ctx *RenderContext) callMaxFunction(args []interface{}) (interface{}, erro
}
}
}
-
+
return max, nil
}
@@ -242,7 +241,7 @@ func (ctx *RenderContext) callMinFunction(args []interface{}) (interface{}, erro
if len(args) < 1 {
return nil, fmt.Errorf("min function requires at least 1 argument")
}
-
+
// If the argument is a slice or array, find the min value in it
if len(args) == 1 {
v := reflect.ValueOf(args[0])
@@ -250,13 +249,13 @@ func (ctx *RenderContext) callMinFunction(args []interface{}) (interface{}, erro
if v.Len() == 0 {
return nil, nil
}
-
+
min := v.Index(0).Interface()
minNum, ok := ctx.toNumber(min)
if !ok {
return min, nil
}
-
+
for i := 1; i < v.Len(); i++ {
val := v.Index(i).Interface()
if valNum, ok := ctx.toNumber(val); ok {
@@ -266,18 +265,18 @@ func (ctx *RenderContext) callMinFunction(args []interface{}) (interface{}, erro
}
}
}
-
+
return min, nil
}
}
-
+
// Find the min value in the arguments
min := args[0]
minNum, ok := ctx.toNumber(min)
if !ok {
return min, nil
}
-
+
for i := 1; i < len(args); i++ {
val := args[i]
if valNum, ok := ctx.toNumber(val); ok {
@@ -287,7 +286,7 @@ func (ctx *RenderContext) callMinFunction(args []interface{}) (interface{}, erro
}
}
}
-
+
return min, nil
}
@@ -296,68 +295,68 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
switch n := node.(type) {
case *LiteralNode:
return n.value, nil
-
+
case *VariableNode:
// Check if it's a macro first
if macro, ok := ctx.GetMacro(n.name); ok {
return macro, nil
}
-
+
// Otherwise, look up variable
return ctx.GetVariable(n.name)
-
+
case *GetAttrNode:
obj, err := ctx.EvaluateExpression(n.node)
if err != nil {
return nil, err
}
-
+
attrName, err := ctx.EvaluateExpression(n.attribute)
if err != nil {
return nil, err
}
-
+
attrStr, ok := attrName.(string)
if !ok {
return nil, fmt.Errorf("attribute name must be a string")
}
-
+
// Check if obj is a map containing macros (from import)
if moduleMap, ok := obj.(map[string]interface{}); ok {
if macro, ok := moduleMap[attrStr]; ok {
return macro, nil
}
}
-
+
return ctx.getAttribute(obj, attrStr)
-
+
case *BinaryNode:
left, err := ctx.EvaluateExpression(n.left)
if err != nil {
return nil, err
}
-
+
right, err := ctx.EvaluateExpression(n.right)
if err != nil {
return nil, err
}
-
+
return ctx.evaluateBinaryOp(n.operator, left, right)
-
+
case *ConditionalNode:
// Evaluate the condition
condResult, err := ctx.EvaluateExpression(n.condition)
if err != nil {
return nil, err
}
-
+
// If condition is true, evaluate the true expression, otherwise evaluate the false expression
if ctx.toBool(condResult) {
return ctx.EvaluateExpression(n.trueExpr)
} else {
return ctx.EvaluateExpression(n.falseExpr)
}
-
+
case *ArrayNode:
// Evaluate each item in the array
items := make([]interface{}, len(n.items))
@@ -369,7 +368,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
items[i] = val
}
return items, nil
-
+
case *FunctionNode:
// Check if it's a macro call
if macro, ok := ctx.GetMacro(n.name); ok {
@@ -382,7 +381,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
}
args[i] = val
}
-
+
// Return a callable that can be rendered later
return func(w io.Writer) error {
macroNode, ok := macro.(*MacroNode)
@@ -392,7 +391,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
return macroNode.Call(w, ctx, args)
}, nil
}
-
+
// Otherwise, it's a regular function call
// Evaluate arguments
args := make([]interface{}, len(n.args))
@@ -403,20 +402,20 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
}
args[i] = val
}
-
+
return ctx.CallFunction(n.name, args)
-
+
case *FilterNode:
// Use the optimized filter chain implementation from render_filter.go
return ctx.evaluateFilterNode(n)
-
+
case *TestNode:
// Evaluate the tested value
value, err := ctx.EvaluateExpression(n.node)
if err != nil {
return nil, err
}
-
+
// Evaluate test arguments
args := make([]interface{}, len(n.args))
for i, arg := range n.args {
@@ -426,7 +425,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
}
args[i] = val
}
-
+
// Look for the test in the environment
if ctx.env != nil {
if test, ok := ctx.env.tests[n.test]; ok {
@@ -434,16 +433,16 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
return test(value, args...)
}
}
-
+
return false, fmt.Errorf("test '%s' not found", n.test)
-
+
case *UnaryNode:
// Evaluate the operand
operand, err := ctx.EvaluateExpression(n.node)
if err != nil {
return nil, err
}
-
+
// Apply the operator
switch n.operator {
case "not", "!":
@@ -461,7 +460,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
default:
return nil, fmt.Errorf("unsupported unary operator: %s", n.operator)
}
-
+
default:
return nil, fmt.Errorf("unsupported expression type: %T", node)
}
@@ -472,7 +471,7 @@ func (ctx *RenderContext) getAttribute(obj interface{}, attr string) (interface{
if obj == nil {
return nil, fmt.Errorf("%w: cannot get attribute %s of nil", ErrInvalidAttribute, attr)
}
-
+
// Handle maps
if objMap, ok := obj.(map[string]interface{}); ok {
if value, exists := objMap[attr]; exists {
@@ -480,15 +479,15 @@ func (ctx *RenderContext) getAttribute(obj interface{}, attr string) (interface{
}
return nil, fmt.Errorf("%w: map has no key %s", ErrInvalidAttribute, attr)
}
-
+
// Use reflection for structs
objValue := reflect.ValueOf(obj)
-
+
// Handle pointer indirection
if objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
}
-
+
// Handle structs
if objValue.Kind() == reflect.Struct {
// Try field access first
@@ -496,7 +495,7 @@ func (ctx *RenderContext) getAttribute(obj interface{}, attr string) (interface{
if field.IsValid() && field.CanInterface() {
return field.Interface(), nil
}
-
+
// Try method access (both with and without parameters)
method := objValue.MethodByName(attr)
if method.IsValid() {
@@ -508,7 +507,7 @@ func (ctx *RenderContext) getAttribute(obj interface{}, attr string) (interface{
return nil, nil
}
}
-
+
// Try method on pointer to struct
ptrValue := reflect.New(objValue.Type())
ptrValue.Elem().Set(objValue)
@@ -523,7 +522,7 @@ func (ctx *RenderContext) getAttribute(obj interface{}, attr string) (interface{
}
}
}
-
+
return nil, fmt.Errorf("%w: %s", ErrInvalidAttribute, attr)
}
@@ -538,28 +537,28 @@ func (ctx *RenderContext) evaluateBinaryOp(operator string, left, right interfac
}
return lStr + ctx.ToString(right), nil
}
-
+
// Handle numeric addition
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum + rNum, nil
}
}
-
+
case "-":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum - rNum, nil
}
}
-
+
case "*":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum * rNum, nil
}
}
-
+
case "/":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
@@ -569,55 +568,55 @@ func (ctx *RenderContext) evaluateBinaryOp(operator string, left, right interfac
return lNum / rNum, nil
}
}
-
+
case "==":
return ctx.equals(left, right), nil
-
+
case "!=":
return !ctx.equals(left, right), nil
-
+
case "<":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum < rNum, nil
}
}
-
+
case ">":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum > rNum, nil
}
}
-
+
case "<=":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum <= rNum, nil
}
}
-
+
case ">=":
if lNum, lok := ctx.toNumber(left); lok {
if rNum, rok := ctx.toNumber(right); rok {
return lNum >= rNum, nil
}
}
-
+
case "and", "&&":
return ctx.toBool(left) && ctx.toBool(right), nil
-
+
case "or", "||":
return ctx.toBool(left) || ctx.toBool(right), nil
-
+
case "~":
// String concatenation
return ctx.ToString(left) + ctx.ToString(right), nil
-
+
case "in":
// Check if left is in right (for arrays, slices, maps, strings)
return ctx.contains(right, left)
-
+
case "not in":
// Check if left is not in right
contains, err := ctx.contains(right, left)
@@ -625,33 +624,33 @@ func (ctx *RenderContext) evaluateBinaryOp(operator string, left, right interfac
return false, err
}
return !contains, nil
-
+
case "matches":
// Regular expression match
pattern := ctx.ToString(right)
str := ctx.ToString(left)
-
+
// Compile the regex
regex, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid regular expression: %s", err)
}
-
+
return regex.MatchString(str), nil
-
+
case "starts with":
// String prefix check
str := ctx.ToString(left)
prefix := ctx.ToString(right)
return strings.HasPrefix(str, prefix), nil
-
+
case "ends with":
// String suffix check
str := ctx.ToString(left)
suffix := ctx.ToString(right)
return strings.HasSuffix(str, suffix), nil
}
-
+
return nil, fmt.Errorf("unsupported binary operator: %s", operator)
}
@@ -660,9 +659,9 @@ func (ctx *RenderContext) contains(container, item interface{}) (bool, error) {
if container == nil {
return false, nil
}
-
+
itemStr := ctx.ToString(item)
-
+
// Handle different container types
switch c := container.(type) {
case string:
@@ -699,7 +698,7 @@ func (ctx *RenderContext) contains(container, item interface{}) (bool, error) {
}
}
}
-
+
return false, nil
}
@@ -711,14 +710,14 @@ func (ctx *RenderContext) equals(a, b interface{}) bool {
if a == nil || b == nil {
return false
}
-
+
// Try numeric comparison
if aNum, aok := ctx.toNumber(a); aok {
if bNum, bok := ctx.toNumber(b); bok {
return aNum == bNum
}
}
-
+
// Try string comparison
return ctx.ToString(a) == ctx.ToString(b)
}
@@ -728,7 +727,7 @@ func (ctx *RenderContext) toNumber(val interface{}) (float64, bool) {
if val == nil {
return 0, false
}
-
+
switch v := val.(type) {
case int:
return float64(v), true
@@ -766,7 +765,7 @@ func (ctx *RenderContext) toNumber(val interface{}) (float64, bool) {
}
return 0, true
}
-
+
// Try reflection for custom types
rv := reflect.ValueOf(val)
switch rv.Kind() {
@@ -777,7 +776,7 @@ func (ctx *RenderContext) toNumber(val interface{}) (float64, bool) {
case reflect.Float32, reflect.Float64:
return rv.Float(), true
}
-
+
return 0, false
}
@@ -786,7 +785,7 @@ func (ctx *RenderContext) toBool(val interface{}) bool {
if val == nil {
return false
}
-
+
switch v := val.(type) {
case bool:
return v
@@ -803,7 +802,7 @@ func (ctx *RenderContext) toBool(val interface{}) bool {
case map[string]interface{}:
return len(v) > 0
}
-
+
// Try reflection for other types
rv := reflect.ValueOf(val)
switch rv.Kind() {
@@ -820,7 +819,7 @@ func (ctx *RenderContext) toBool(val interface{}) bool {
case reflect.Array, reflect.Slice, reflect.Map:
return rv.Len() > 0
}
-
+
// Default to true for other non-nil values
return true
}
@@ -830,7 +829,7 @@ func (ctx *RenderContext) ToString(val interface{}) string {
if val == nil {
return ""
}
-
+
switch v := val.(type) {
case string:
return v
@@ -853,6 +852,6 @@ func (ctx *RenderContext) ToString(val interface{}) string {
case fmt.Stringer:
return v.String()
}
-
+
return fmt.Sprintf("%v", val)
-}
\ No newline at end of file
+}
diff --git a/render_filter.go b/render_filter.go
index 0069e06..9140452 100644
--- a/render_filter.go
+++ b/render_filter.go
@@ -27,7 +27,7 @@ type FilterChainItem struct {
func (ctx *RenderContext) DetectFilterChain(node Node) (Node, []FilterChainItem, error) {
var chain []FilterChainItem
currentNode := node
-
+
// Traverse down the filter chain, collecting filters as we go
for {
// Check if the current node is a filter node
@@ -36,7 +36,7 @@ func (ctx *RenderContext) DetectFilterChain(node Node) (Node, []FilterChainItem,
// We've reached the base value node
break
}
-
+
// Evaluate filter arguments
args := make([]interface{}, len(filterNode.args))
for i, arg := range filterNode.args {
@@ -46,17 +46,17 @@ func (ctx *RenderContext) DetectFilterChain(node Node) (Node, []FilterChainItem,
}
args[i] = val
}
-
+
// Add this filter to the chain (prepend to maintain order)
chain = append([]FilterChainItem{{
name: filterNode.filter,
args: args,
}}, chain...)
-
+
// Continue with the next node in the chain
currentNode = filterNode.node
}
-
+
return currentNode, chain, nil
}
@@ -65,7 +65,7 @@ func (ctx *RenderContext) ApplyFilterChain(baseValue interface{}, chain []Filter
// Start with the base value
result := baseValue
var err error
-
+
// Apply each filter in the chain
for _, filter := range chain {
result, err = ctx.ApplyFilter(filter.name, result, filter.args...)
@@ -73,7 +73,7 @@ func (ctx *RenderContext) ApplyFilterChain(baseValue interface{}, chain []Filter
return nil, err
}
}
-
+
return result, nil
}
@@ -84,13 +84,13 @@ func (ctx *RenderContext) evaluateFilterNode(n *FilterNode) (interface{}, error)
if err != nil {
return nil, err
}
-
+
// Evaluate the base value
value, err := ctx.EvaluateExpression(baseNode)
if err != nil {
return nil, err
}
-
+
// Apply the entire filter chain in a single operation
return ctx.ApplyFilterChain(value, filterChain)
-}
\ No newline at end of file
+}
diff --git a/tokenizer.go b/tokenizer.go
index 663dc01..dcf6333 100644
--- a/tokenizer.go
+++ b/tokenizer.go
@@ -1,25 +1,25 @@
package twig
import (
- "strings"
"regexp"
+ "strings"
)
// Special processor for HTML attributes with embedded Twig variables
func processHtmlAttributesWithTwigVars(source string) []Token {
var tokens []Token
line := 1
-
+
// Break up the HTML tag with embedded variables
// For example:
-
+
// First, find all the attribute pairs
for len(source) > 0 {
// Count newlines for line tracking
if idx := strings.IndexByte(source, '\n'); idx >= 0 {
line += strings.Count(source[:idx], "\n")
}
-
+
// Look for the attribute pattern: attr="{{ var }}"
attrNameEnd := strings.Index(source, "=\"{{")
if attrNameEnd < 0 {
@@ -29,10 +29,10 @@ func processHtmlAttributesWithTwigVars(source string) []Token {
}
break
}
-
+
// Get the attribute name
attrName := source[:attrNameEnd]
-
+
// Find the end of the embedded variable
varEnd := strings.Index(source[attrNameEnd+3:], "}}")
if varEnd < 0 {
@@ -41,20 +41,20 @@ func processHtmlAttributesWithTwigVars(source string) []Token {
break
}
varEnd += attrNameEnd + 3
-
+
// Extract the variable name (inside {{ }})
- varName := strings.TrimSpace(source[attrNameEnd+3:varEnd])
-
+ varName := strings.TrimSpace(source[attrNameEnd+3 : varEnd])
+
// Add tokens: text for the attribute name, then VAR_START, var name, VAR_END
if attrNameEnd > 0 {
tokens = append(tokens, Token{Type: TOKEN_TEXT, Value: attrName + "=\"", Line: line})
}
-
+
// Add variable tokens
tokens = append(tokens, Token{Type: TOKEN_VAR_START, Line: line})
tokens = append(tokens, Token{Type: TOKEN_NAME, Value: varName, Line: line})
tokens = append(tokens, Token{Type: TOKEN_VAR_END, Line: line})
-
+
// Find the closing quote and add the rest as text
quoteEnd := strings.Index(source[varEnd+2:], "\"")
if quoteEnd < 0 {
@@ -63,14 +63,14 @@ func processHtmlAttributesWithTwigVars(source string) []Token {
break
}
quoteEnd += varEnd + 2
-
+
// Add the closing quote
tokens = append(tokens, Token{Type: TOKEN_TEXT, Value: "\"", Line: line})
-
+
// Move past this attribute
source = source[quoteEnd+1:]
}
-
+
return tokens
}
@@ -81,14 +81,14 @@ func processWhitespaceControl(tokens []Token) []Token {
if len(tokens) == 0 {
return tokens
}
-
+
var result []Token = make([]Token, len(tokens))
copy(result, tokens)
-
+
// Process each token to apply whitespace trimming
for i := 0; i < len(result); i++ {
token := result[i]
-
+
// Handle opening tags that trim whitespace before them
if token.Type == TOKEN_VAR_START_TRIM || token.Type == TOKEN_BLOCK_START_TRIM {
// If there's a text token before this, trim its trailing whitespace
@@ -96,7 +96,7 @@ func processWhitespaceControl(tokens []Token) []Token {
result[i-1].Value = trimTrailingWhitespace(result[i-1].Value)
}
}
-
+
// Handle closing tags that trim whitespace after them
if token.Type == TOKEN_VAR_END_TRIM || token.Type == TOKEN_BLOCK_END_TRIM {
// If there's a text token after this, trim its leading whitespace
@@ -105,7 +105,7 @@ func processWhitespaceControl(tokens []Token) []Token {
}
}
}
-
+
return result
}
@@ -114,4 +114,4 @@ func processSpacelessTag(content string) string {
// This regex matches whitespace between HTML tags
re := regexp.MustCompile(">\\s+<")
return re.ReplaceAllString(content, "><")
-}
\ No newline at end of file
+}
diff --git a/twig.go b/twig.go
index ccfdd21..a6c8c25 100644
--- a/twig.go
+++ b/twig.go
@@ -4,6 +4,7 @@ package twig
import (
"bytes"
+ "errors"
"io"
"sync"
"time"
@@ -17,31 +18,31 @@ type Engine struct {
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
+ 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
+ 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
@@ -50,11 +51,11 @@ type Environment struct {
// 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),
+ 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
@@ -63,12 +64,12 @@ func New() *Engine {
engine := &Engine{
templates: make(map[string]*Template),
environment: env,
- autoReload: false, // Disable auto-reload by default
+ autoReload: false, // Disable auto-reload by default
}
-
+
// Register the core extension by default
engine.AddExtension(&CoreExtension{})
-
+
return engine
}
@@ -132,12 +133,12 @@ func (e *Engine) Load(name string) (*Template, error) {
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
@@ -158,20 +159,20 @@ func (e *Engine) Load(name string) (*Template, error) {
// 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 {
@@ -183,7 +184,7 @@ func (e *Engine) Load(name string) (*Template, error) {
source: source,
nodes: nodes,
env: e.environment,
- engine: e, // Add reference to the engine
+ engine: e, // Add reference to the engine
loader: sourceLoader,
lastModified: lastModified,
}
@@ -277,7 +278,7 @@ func (e *Engine) RegisterTemplate(name string, template *Template) {
if template.lastModified == 0 {
template.lastModified = time.Now().Unix()
}
-
+
// Only cache if caching is enabled
if e.environment.cache {
e.mu.Lock()
@@ -286,6 +287,47 @@ func (e *Engine) RegisterTemplate(name string, template *Template) {
}
}
+// 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
@@ -309,27 +351,27 @@ func (e *Engine) AddGlobal(name string, value interface{}) {
// 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)
}
@@ -343,7 +385,7 @@ func (e *Engine) CreateExtension(name string) *CustomExtension {
Tests: make(map[string]TestFunc),
Operators: make(map[string]OperatorFunc),
}
-
+
return extension
}
@@ -399,7 +441,7 @@ func (e *Engine) ParseTemplate(source string) (*Template, error) {
if e.Parse != nil {
return e.Parse(source)
}
-
+
parser := &Parser{}
nodes, err := parser.Parse(source)
if err != nil {
@@ -433,7 +475,7 @@ 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,
@@ -444,10 +486,27 @@ func (t *Template) RenderTo(w io.Writer, context map[string]interface{}) error {
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
@@ -461,4 +520,4 @@ func (b *StringBuffer) Write(p []byte) (n int, err error) {
// String returns the buffer's contents as a string
func (b *StringBuffer) String() string {
return b.buf.String()
-}
\ No newline at end of file
+}
diff --git a/twig_test.go b/twig_test.go
index 283c496..029d16a 100644
--- a/twig_test.go
+++ b/twig_test.go
@@ -10,33 +10,33 @@ import (
func TestBasicTemplate(t *testing.T) {
engine := New()
-
+
// Let's simplify for now - just fake the parsing
text := "Hello, World!"
node := NewTextNode(text, 1)
root := NewRootNode([]Node{node}, 1)
-
+
template := &Template{
name: "simple",
source: text,
nodes: root,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["simple"] = template
engine.mu.Unlock()
-
+
// Render with context
context := map[string]interface{}{
"name": "World",
}
-
+
result, err := engine.Render("simple", context)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
-
+
expected := "Hello, World!"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
@@ -45,34 +45,34 @@ func TestBasicTemplate(t *testing.T) {
func TestRenderToWriter(t *testing.T) {
engine := New()
-
+
// Let's simplify for now - just fake the parsing
text := "Value: 42"
node := NewTextNode(text, 1)
root := NewRootNode([]Node{node}, 1)
-
+
template := &Template{
name: "writer_test",
source: text,
nodes: root,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["writer_test"] = template
engine.mu.Unlock()
-
+
// Render with context to a buffer
context := map[string]interface{}{
"value": 42,
}
-
+
var buf bytes.Buffer
err := engine.RenderTo(&buf, "writer_test", context)
if err != nil {
t.Fatalf("Error rendering template to writer: %v", err)
}
-
+
expected := "Value: 42"
if buf.String() != expected {
t.Errorf("Expected result to be %q, but got %q", expected, buf.String())
@@ -81,11 +81,11 @@ func TestRenderToWriter(t *testing.T) {
func TestTemplateNotFound(t *testing.T) {
engine := New()
-
+
// Create empty array loader
loader := NewArrayLoader(map[string]string{})
engine.RegisterLoader(loader)
-
+
// Try to render non-existent template
_, err := engine.Render("nonexistent", nil)
if err == nil {
@@ -95,23 +95,23 @@ func TestTemplateNotFound(t *testing.T) {
func TestVariableAccess(t *testing.T) {
engine := New()
-
+
// Let's simplify for now - just fake the parsing
text := "Name: John, Age: 30"
node := NewTextNode(text, 1)
root := NewRootNode([]Node{node}, 1)
-
+
template := &Template{
name: "nested",
source: text,
nodes: root,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["nested"] = template
engine.mu.Unlock()
-
+
// Render with nested context
context := map[string]interface{}{
"user": map[string]interface{}{
@@ -119,12 +119,12 @@ func TestVariableAccess(t *testing.T) {
"age": 30,
},
}
-
+
result, err := engine.Render("nested", context)
if err != nil {
t.Fatalf("Error rendering template with nested variables: %v", err)
}
-
+
expected := "Name: John, Age: 30"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
@@ -133,7 +133,7 @@ func TestVariableAccess(t *testing.T) {
func TestForLoop(t *testing.T) {
engine := New()
-
+
// Create a for loop template manually
itemsNode := NewVariableNode("items", 1)
loopBody := []Node{
@@ -141,7 +141,7 @@ func TestForLoop(t *testing.T) {
NewVariableNode("item", 1),
NewTextNode("", 1),
}
-
+
forNode := &ForNode{
keyVar: "",
valueVar: "item",
@@ -150,57 +150,57 @@ func TestForLoop(t *testing.T) {
elseBranch: []Node{NewTextNode("No items", 1)},
line: 1,
}
-
+
// Wrap in a simple list
rootNodes := []Node{
NewTextNode("", 1),
forNode,
NewTextNode("
", 1),
}
-
+
root := NewRootNode(rootNodes, 1)
-
+
template := &Template{
name: "for_test",
source: "{% for item in items %}{{ item }}{% else %}No items{% endfor %}",
nodes: root,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["for_test"] = template
engine.mu.Unlock()
-
+
// Test with non-empty array
context := map[string]interface{}{
"items": []string{"apple", "banana", "orange"},
}
-
+
result, err := engine.Render("for_test", context)
if err != nil {
t.Fatalf("Error rendering for template (with items): %v", err)
}
-
+
expected := ""
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
}
-
+
// Test with empty array
context = map[string]interface{}{
"items": []string{},
}
-
+
result, err = engine.Render("for_test", context)
if err != nil {
t.Fatalf("Error rendering for template (empty items): %v", err)
}
-
+
expected = ""
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
}
-
+
// Test with map
mapItemsNode := NewVariableNode("map", 1)
mapLoopBody := []Node{
@@ -210,28 +210,28 @@ func TestForLoop(t *testing.T) {
NewVariableNode("value", 1),
NewTextNode("", 1),
}
-
+
mapForNode := NewForNode("key", "value", mapItemsNode, mapLoopBody, nil, 1)
-
+
mapRootNodes := []Node{
NewTextNode("", 1),
mapForNode,
NewTextNode("
", 1),
}
-
+
mapRoot := NewRootNode(mapRootNodes, 1)
-
+
mapTemplate := &Template{
name: "for_map_test",
source: "{% for key, value in map %}{{ key }}: {{ value }}{% endfor %}",
nodes: mapRoot,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["for_map_test"] = mapTemplate
engine.mu.Unlock()
-
+
context = map[string]interface{}{
"map": map[string]string{
"name": "John",
@@ -239,7 +239,7 @@ func TestForLoop(t *testing.T) {
"country": "USA",
},
}
-
+
// For maps, we won't check exact output as order is not guaranteed
_, err = engine.Render("for_map_test", context)
if err != nil {
@@ -249,7 +249,7 @@ func TestForLoop(t *testing.T) {
func TestInclude(t *testing.T) {
engine := New()
-
+
// Create a partial template
partialBody := []Node{
NewTextNode("\n", 1),
@@ -261,9 +261,9 @@ func TestInclude(t *testing.T) {
NewTextNode("\n
\n", 3),
NewTextNode("", 4),
}
-
+
partialRoot := NewRootNode(partialBody, 1)
-
+
partialTemplate := &Template{
name: "widget.html",
source: "partial template source",
@@ -271,21 +271,21 @@ func TestInclude(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Create a main template that includes the partial
variables := map[string]Node{
"title": NewLiteralNode("Latest News", 2),
"content": NewLiteralNode("Here is the latest news content.", 2),
}
-
+
includeNode := NewIncludeNode(
NewLiteralNode("widget.html", 1),
variables,
- false, // ignoreMissing
- false, // only
+ false, // ignoreMissing
+ false, // only
1,
)
-
+
mainBody := []Node{
NewTextNode("\n\n\n", 1),
NewTextNode(" Main Page
\n", 2),
@@ -294,9 +294,9 @@ func TestInclude(t *testing.T) {
NewTextNode("\n \n", 4),
NewTextNode("\n", 5),
}
-
+
mainRoot := NewRootNode(mainBody, 1)
-
+
mainTemplate := &Template{
name: "main.html",
source: "main template source",
@@ -304,44 +304,44 @@ func TestInclude(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Add the templates to the engine
engine.mu.Lock()
engine.templates["widget.html"] = partialTemplate
engine.templates["main.html"] = mainTemplate
engine.mu.Unlock()
-
+
// Render the main template
context := map[string]interface{}{
"globalVar": "I'm global",
}
-
+
result, err := engine.Render("main.html", context)
if err != nil {
t.Fatalf("Error rendering template with include: %v", err)
}
-
+
expected := "\n\n\n Main Page
\n \n\n"
-
+
if result != expected {
t.Errorf("Include failed. Expected:\n%s\n\nGot:\n%s", expected, result)
}
-
+
// Test with 'only'
onlyIncludeNode := NewIncludeNode(
NewLiteralNode("widget.html", 1),
map[string]Node{
"title": NewLiteralNode("Only Title", 2),
- "content": NewLiteralNode("Local content", 2), // Use local content instead of global
+ "content": NewLiteralNode("Local content", 2), // Use local content instead of global
},
- false, // ignoreMissing
- true, // only
+ false, // ignoreMissing
+ true, // only
1,
)
-
+
onlyBody := []Node{onlyIncludeNode}
onlyRoot := NewRootNode(onlyBody, 1)
-
+
onlyTemplate := &Template{
name: "only_test.html",
source: "only test source",
@@ -349,20 +349,20 @@ func TestInclude(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.mu.Lock()
engine.templates["only_test.html"] = onlyTemplate
engine.mu.Unlock()
-
+
// With 'only', just use the explicitly provided variables
result, err = engine.Render("only_test.html", context)
if err != nil {
t.Fatalf("Error rendering template with 'only': %v", err)
}
-
+
// Now expected content is the local content, not an empty string
expected = ""
-
+
if result != expected {
t.Errorf("Include with 'only' failed. Expected:\n%s\n\nGot:\n%s", expected, result)
}
@@ -370,7 +370,7 @@ func TestInclude(t *testing.T) {
func TestBlockInheritance(t *testing.T) {
engine := New()
-
+
// Create a base template
baseBody := []Node{
NewTextNode("\n\n\n ", 1),
@@ -381,9 +381,9 @@ func TestBlockInheritance(t *testing.T) {
NewBlockNode("footer", []Node{NewTextNode("Default Footer", 6)}, 6),
NewTextNode("\n \n\n", 7),
}
-
+
baseRoot := NewRootNode(baseBody, 1)
-
+
baseTemplate := &Template{
name: "base.html",
source: "base template source",
@@ -391,16 +391,16 @@ func TestBlockInheritance(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Create a child template
childBody := []Node{
NewExtendsNode(NewLiteralNode("base.html", 1), 1),
NewBlockNode("title", []Node{NewTextNode("Child Page Title", 2)}, 2),
NewBlockNode("content", []Node{NewTextNode("This is the child content.", 3)}, 3),
}
-
+
childRoot := NewRootNode(childBody, 1)
-
+
childTemplate := &Template{
name: "child.html",
source: "child template source",
@@ -408,24 +408,24 @@ func TestBlockInheritance(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Add both templates to the engine
engine.mu.Lock()
engine.templates["base.html"] = baseTemplate
engine.templates["child.html"] = childTemplate
engine.mu.Unlock()
-
+
// Render the child template (which should use the base template)
context := map[string]interface{}{}
-
+
result, err := engine.Render("child.html", context)
if err != nil {
t.Fatalf("Error rendering template with inheritance: %v", err)
}
-
+
// Check that the child blocks were properly injected into the base template
expected := "\n\n\n Child Page Title\n\n\n \n This is the child content.\n
\n \n\n"
-
+
if result != expected {
t.Errorf("Template inheritance failed. Expected:\n%s\n\nGot:\n%s", expected, result)
}
@@ -433,57 +433,57 @@ func TestBlockInheritance(t *testing.T) {
func TestIfStatement(t *testing.T) {
engine := New()
-
+
// Create an if statement template manually
condition := NewVariableNode("show", 1)
trueBody := []Node{NewTextNode("Content is visible", 1)}
elseBody := []Node{NewTextNode("Content is hidden", 1)}
-
+
ifNode := &IfNode{
conditions: []Node{condition},
bodies: [][]Node{trueBody},
elseBranch: elseBody,
line: 1,
}
-
+
root := NewRootNode([]Node{ifNode}, 1)
-
+
template := &Template{
name: "if_test",
source: "{% if show %}Content is visible{% else %}Content is hidden{% endif %}",
nodes: root,
env: engine.environment,
}
-
+
engine.mu.Lock()
engine.templates["if_test"] = template
engine.mu.Unlock()
-
+
// Test with condition = true
context := map[string]interface{}{
"show": true,
}
-
+
result, err := engine.Render("if_test", context)
if err != nil {
t.Fatalf("Error rendering if template (true condition): %v", err)
}
-
+
expected := "Content is visible"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
}
-
+
// Test with condition = false
context = map[string]interface{}{
"show": false,
}
-
+
result, err = engine.Render("if_test", context)
if err != nil {
t.Fatalf("Error rendering if template (false condition): %v", err)
}
-
+
expected = "Content is hidden"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
@@ -492,17 +492,17 @@ func TestIfStatement(t *testing.T) {
func TestSetTag(t *testing.T) {
engine := New()
-
+
// Create a parser to parse a template string
parser := &Parser{}
source := "{% set greeting = 'Hello, Twig!' %}{{ greeting }}"
-
+
// Parse the template
node, err := parser.Parse(source)
if err != nil {
t.Fatalf("Error parsing set template: %v", err)
}
-
+
// Create a template with the parsed nodes
template := &Template{
name: "set_test",
@@ -511,30 +511,30 @@ func TestSetTag(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Register the template
engine.RegisterTemplate("set_test", template)
-
+
// Render with an empty context
context := map[string]interface{}{}
-
+
result, err := engine.Render("set_test", context)
if err != nil {
t.Fatalf("Error rendering set template: %v", err)
}
-
+
expected := "Hello, Twig!"
if result != expected {
t.Errorf("Expected result to be %q, but got %q", expected, result)
}
-
+
// Test setting with an expression
exprSource := "{% set num = 5 + 10 %}{{ num }}"
exprNode, err := parser.Parse(exprSource)
if err != nil {
t.Fatalf("Error parsing expression template: %v", err)
}
-
+
// Create a template with the parsed nodes
exprTemplate := &Template{
name: "expr_test",
@@ -543,16 +543,16 @@ func TestSetTag(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Register the template
engine.RegisterTemplate("expr_test", exprTemplate)
-
+
// Render with an empty context
exprResult, err := engine.Render("expr_test", context)
if err != nil {
t.Fatalf("Error rendering expression template: %v", err)
}
-
+
exprExpected := "15"
if exprResult != exprExpected {
t.Errorf("Expected result to be %q, but got %q", exprExpected, exprResult)
@@ -561,10 +561,10 @@ func TestSetTag(t *testing.T) {
func TestFilters(t *testing.T) {
engine := New()
-
+
// Create a parser to parse templates with filters
parser := &Parser{}
-
+
// Test cases for different filter scenarios
testCases := []struct {
name string
@@ -603,7 +603,7 @@ func TestFilters(t *testing.T) {
context: nil,
expected: "A, B, C",
},
-
+
// Complex filter usage
{
name: "filter_in_expression",
@@ -635,7 +635,7 @@ func TestFilters(t *testing.T) {
context: nil,
expected: "hello",
},
-
+
// Common filter use cases
{
name: "escape_filter",
@@ -667,7 +667,7 @@ func TestFilters(t *testing.T) {
context: nil,
expected: "heyyx wxryd",
},
-
+
// Special cases
{
name: "filter_on_function_result",
@@ -700,14 +700,14 @@ func TestFilters(t *testing.T) {
expected: "hello",
},
}
-
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
node, err := parser.Parse(tc.source)
if err != nil {
t.Fatalf("Error parsing template with filter: %v", err)
}
-
+
template := &Template{
name: tc.name,
source: tc.source,
@@ -715,14 +715,14 @@ func TestFilters(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate(tc.name, template)
-
+
result, err := engine.Render(tc.name, tc.context)
if err != nil {
t.Fatalf("Error rendering template with filter: %v", err)
}
-
+
if result != tc.expected {
t.Errorf("Expected result to be %q, but got %q", tc.expected, result)
}
@@ -732,10 +732,10 @@ func TestFilters(t *testing.T) {
func TestFunctions(t *testing.T) {
engine := New()
-
+
// Create a parser to parse a template with functions
parser := &Parser{}
-
+
// Test basic function parsing without worrying about exact output for now
testCases := []struct {
name string
@@ -745,14 +745,14 @@ func TestFunctions(t *testing.T) {
{"function_with_args", "{{ min(10, 5, 8, 2, 15) }}"},
{"function_with_complex_args", "{{ merge([1, 2], [3, 4], [5, 6]) }}"},
}
-
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
node, err := parser.Parse(tc.source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
template := &Template{
name: tc.name,
source: tc.source,
@@ -760,9 +760,9 @@ func TestFunctions(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate(tc.name, template)
-
+
// Just check that the template renders without errors
_, err = engine.Render(tc.name, nil)
if err != nil {
@@ -774,17 +774,17 @@ func TestFunctions(t *testing.T) {
func TestIsTests(t *testing.T) {
engine := New()
-
+
// Create a parser to parse a template with is tests
parser := &Parser{}
-
+
// Test 'is defined' test
source := "{% if variable is defined %}defined{% else %}not defined{% endif %}"
node, err := parser.Parse(source)
if err != nil {
t.Fatalf("Error parsing 'is defined' test template: %v", err)
}
-
+
template := &Template{
name: "is_defined_test",
source: source,
@@ -792,28 +792,28 @@ func TestIsTests(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate("is_defined_test", template)
-
+
// Test with undefined variable - run but don't check the exact output
_, err = engine.Render("is_defined_test", nil)
if err != nil {
t.Fatalf("Error rendering 'is defined' test template with undefined variable: %v", err)
}
-
+
// Test with defined variable - run but don't check the exact output
_, err = engine.Render("is_defined_test", map[string]interface{}{"variable": "value"})
if err != nil {
t.Fatalf("Error rendering 'is defined' test template with defined variable: %v", err)
}
-
+
// Test 'is empty' test
emptySource := "{% if array is empty %}empty{% else %}not empty{% endif %}"
emptyNode, err := parser.Parse(emptySource)
if err != nil {
t.Fatalf("Error parsing 'is empty' test template: %v", err)
}
-
+
emptyTemplate := &Template{
name: "is_empty_test",
source: emptySource,
@@ -821,28 +821,28 @@ func TestIsTests(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate("is_empty_test", emptyTemplate)
-
+
// Test with empty array - run but don't check the exact output
_, err = engine.Render("is_empty_test", map[string]interface{}{"array": []string{}})
if err != nil {
t.Fatalf("Error rendering 'is empty' test template with empty array: %v", err)
}
-
+
// Test with non-empty array - run but don't check the exact output
_, err = engine.Render("is_empty_test", map[string]interface{}{"array": []string{"a", "b"}})
if err != nil {
t.Fatalf("Error rendering 'is empty' test template with non-empty array: %v", err)
}
-
+
// Test 'is not' syntax
notSource := "{% if value is not empty %}not empty{% else %}empty{% endif %}"
notNode, err := parser.Parse(notSource)
if err != nil {
t.Fatalf("Error parsing 'is not' test template: %v", err)
}
-
+
notTemplate := &Template{
name: "is_not_test",
source: notSource,
@@ -850,9 +850,9 @@ func TestIsTests(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate("is_not_test", notTemplate)
-
+
// Test with non-empty value - run but don't check the exact output
_, err = engine.Render("is_not_test", map[string]interface{}{"value": "something"})
if err != nil {
@@ -862,17 +862,17 @@ func TestIsTests(t *testing.T) {
func TestOperators(t *testing.T) {
engine := New()
-
+
// Create a parser to parse a template with operators
parser := &Parser{}
-
+
// Test standard operators (simpler test for now)
source := "{{ 5 + 3 }}"
node, err := parser.Parse(source)
if err != nil {
t.Fatalf("Error parsing operator template: %v", err)
}
-
+
template := &Template{
name: "operator_test",
source: source,
@@ -880,14 +880,14 @@ func TestOperators(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate("operator_test", template)
-
+
_, err = engine.Render("operator_test", nil)
if err != nil {
t.Fatalf("Error rendering operator template: %v", err)
}
-
+
// Test basic operator parsing without worrying about exact output for now
testCases := []struct {
name string
@@ -899,14 +899,14 @@ func TestOperators(t *testing.T) {
{"starts_with_operator", "{% if 'hello' starts with 'he' %}starts with{% else %}does not start with{% endif %}"},
{"ends_with_operator", "{% if 'hello' ends with 'lo' %}ends with{% else %}does not end with{% endif %}"},
}
-
+
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
node, err := parser.Parse(tc.source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
template := &Template{
name: tc.name,
source: tc.source,
@@ -914,9 +914,9 @@ func TestOperators(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
engine.RegisterTemplate(tc.name, template)
-
+
// Just check that the template renders without errors
_, err = engine.Render(tc.name, nil)
if err != nil {
@@ -929,17 +929,17 @@ func TestOperators(t *testing.T) {
func TestFilterChainOptimization(t *testing.T) {
// Create a Twig engine
engine := New()
-
+
// Create a template with a long filter chain
source := "{{ 'hello world'|upper|trim|slice(0, 5)|replace('H', 'J')|upper }}"
-
+
// Parse the template
parser := &Parser{}
node, err := parser.Parse(source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
// Create a template
template := &Template{
source: source,
@@ -947,13 +947,13 @@ func TestFilterChainOptimization(t *testing.T) {
env: engine.environment,
engine: engine,
}
-
+
// Render the template
result, err := template.Render(nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
-
+
// Verify the expected output (should get the same result with optimized chain)
expected := "JELLO"
if result != expected {
@@ -964,17 +964,17 @@ func TestFilterChainOptimization(t *testing.T) {
func BenchmarkFilterChain(b *testing.B) {
// Create a Twig engine
engine := New()
-
+
// Create a template with a long filter chain
source := "{{ 'hello world'|upper|trim|slice(0, 5)|replace('H', 'J') }}"
-
+
// Parse the template
parser := &Parser{}
node, err := parser.Parse(source)
if err != nil {
b.Fatalf("Error parsing template: %v", err)
}
-
+
// Create a template
template := &Template{
source: source,
@@ -982,7 +982,7 @@ func BenchmarkFilterChain(b *testing.B) {
env: engine.environment,
engine: engine,
}
-
+
// Bench the rendering
b.ResetTimer()
for i := 0; i < b.N; i++ {
@@ -996,7 +996,7 @@ func BenchmarkFilterChain(b *testing.B) {
func TestTemplateModificationTime(t *testing.T) {
// Create a temporary directory for template files
tempDir := t.TempDir()
-
+
// Create a test template file
templatePath := filepath.Join(tempDir, "test.twig")
initialContent := "Hello,{{ name }}!"
@@ -1004,23 +1004,23 @@ func TestTemplateModificationTime(t *testing.T) {
if err != nil {
t.Fatalf("Failed to create test template: %v", err)
}
-
+
// Create a Twig engine
engine := New()
-
+
// Register a file system loader pointing to our temp directory
loader := NewFileSystemLoader([]string{tempDir})
engine.RegisterLoader(loader)
-
+
// Enable auto-reload
engine.SetAutoReload(true)
-
+
// First load of the template
template1, err := engine.Load("test")
if err != nil {
t.Fatalf("Failed to load template: %v", err)
}
-
+
// Render the template
result1, err := template1.Render(map[string]interface{}{"name": "World"})
if err != nil {
@@ -1029,80 +1029,80 @@ func TestTemplateModificationTime(t *testing.T) {
if result1 != "Hello,World!" {
t.Errorf("Expected 'Hello,World!', got '%s'", result1)
}
-
+
// Store the first template's timestamp
initialTimestamp := template1.lastModified
-
+
// Load the template again - should use cache since file hasn't changed
template2, err := engine.Load("test")
if err != nil {
t.Fatalf("Failed to load template second time: %v", err)
}
-
+
// Verify we got the same template back (cache hit)
if template2.lastModified != initialTimestamp {
- t.Errorf("Expected same timestamp, got different values: %d vs %d",
+ t.Errorf("Expected same timestamp, got different values: %d vs %d",
initialTimestamp, template2.lastModified)
}
-
+
// Sleep to ensure file modification time will be different
time.Sleep(1 * time.Second)
-
+
// Modify the template file
modifiedContent := "Greetings,{{ name }}!"
err = os.WriteFile(templatePath, []byte(modifiedContent), 0644)
if err != nil {
t.Fatalf("Failed to update test template: %v", err)
}
-
+
// Load the template again - should detect the change and reload
template3, err := engine.Load("test")
if err != nil {
t.Fatalf("Failed to load modified template: %v", err)
}
-
+
// Render the template again
result3, err := template3.Render(map[string]interface{}{"name": "World"})
if err != nil {
t.Fatalf("Failed to render modified template: %v", err)
}
-
+
// Verify we got the updated content
if result3 != "Greetings,World!" {
t.Errorf("Expected 'Greetings,World!', got '%s'", result3)
}
-
+
// Verify the template was reloaded (newer timestamp)
if template3.lastModified <= initialTimestamp {
- t.Errorf("Expected newer timestamp, but got %d <= %d",
+ t.Errorf("Expected newer timestamp, but got %d <= %d",
template3.lastModified, initialTimestamp)
}
-
+
// Disable auto-reload
engine.SetAutoReload(false)
-
+
// Sleep to ensure file modification time will be different
time.Sleep(1 * time.Second)
-
+
// Modify the template file again
finalContent := "Welcome,{{ name }}!"
err = os.WriteFile(templatePath, []byte(finalContent), 0644)
if err != nil {
t.Fatalf("Failed to update test template again: %v", err)
}
-
+
// Load the template again - should NOT detect the change because auto-reload is off
template4, err := engine.Load("test")
if err != nil {
t.Fatalf("Failed to load template with auto-reload off: %v", err)
}
-
+
// Render the template again
result4, err := template4.Render(map[string]interface{}{"name": "World"})
if err != nil {
t.Fatalf("Failed to render template with auto-reload off: %v", err)
}
-
+
// Verify we still have the previous content, not the updated one
if result4 != "Greetings,World!" {
t.Errorf("Expected 'Greetings,World!', got '%s'", result4)
@@ -1112,7 +1112,7 @@ func TestTemplateModificationTime(t *testing.T) {
func TestDevelopmentMode(t *testing.T) {
// Create a new engine
engine := New()
-
+
// Verify default settings
if !engine.environment.cache {
t.Errorf("Cache should be enabled by default")
@@ -1123,10 +1123,10 @@ func TestDevelopmentMode(t *testing.T) {
if engine.autoReload {
t.Errorf("AutoReload should be disabled by default")
}
-
+
// Enable development mode
engine.SetDevelopmentMode(true)
-
+
// Check that the settings were changed correctly
if engine.environment.cache {
t.Errorf("Cache should be disabled in development mode")
@@ -1137,28 +1137,28 @@ func TestDevelopmentMode(t *testing.T) {
if !engine.autoReload {
t.Errorf("AutoReload should be enabled in development mode")
}
-
+
// Create a template source
source := "Hello,{{ name }}!"
-
+
// Create an array loader and register it
loader := NewArrayLoader(map[string]string{
"dev_test.twig": source,
})
engine.RegisterLoader(loader)
-
+
// Parse the template to verify it's valid
parser := &Parser{}
_, err := parser.Parse(source)
if err != nil {
t.Fatalf("Error parsing template: %v", err)
}
-
+
// Verify the template isn't in the cache yet
if len(engine.templates) > 0 {
t.Errorf("Templates map should be empty in development mode, but has %d entries", len(engine.templates))
}
-
+
// In development mode, rendering should work but not cache
result, err := engine.Render("dev_test.twig", map[string]interface{}{
"name": "World",
@@ -1169,10 +1169,10 @@ func TestDevelopmentMode(t *testing.T) {
if result != "Hello,World!" {
t.Errorf("Expected 'Hello,World!', got '%s'", result)
}
-
+
// Disable development mode
engine.SetDevelopmentMode(false)
-
+
// Check that the settings were changed back
if !engine.environment.cache {
t.Errorf("Cache should be enabled when development mode is off")
@@ -1183,7 +1183,7 @@ func TestDevelopmentMode(t *testing.T) {
if engine.autoReload {
t.Errorf("AutoReload should be disabled when development mode is off")
}
-
+
// In production mode, rendering should cache the template
result, err = engine.Render("dev_test.twig", map[string]interface{}{
"name": "World",
@@ -1194,7 +1194,7 @@ func TestDevelopmentMode(t *testing.T) {
if result != "Hello,World!" {
t.Errorf("Expected 'Hello,World!', got '%s'", result)
}
-
+
// Template should now be in the cache
if len(engine.templates) != 1 {
t.Errorf("Templates map should have 1 entry, but has %d", len(engine.templates))
@@ -1202,4 +1202,4 @@ func TestDevelopmentMode(t *testing.T) {
if _, ok := engine.templates["dev_test.twig"]; !ok {
t.Errorf("Template should be in the cache")
}
-}
\ No newline at end of file
+}
diff --git a/whitespace.go b/whitespace.go
index 97509bf..4b6f567 100644
--- a/whitespace.go
+++ b/whitespace.go
@@ -34,19 +34,19 @@ func NewSpacelessNode(body []Node, line int) *SpacelessNode {
func (n *SpacelessNode) Render(w io.Writer, ctx *RenderContext) error {
// First, render the content to a string using a buffer
var buf StringBuffer
-
+
for _, node := range n.body {
if err := node.Render(&buf, ctx); err != nil {
return err
}
}
-
+
// Get the rendered content as a string
content := buf.String()
-
+
// Apply spaceless processing (remove whitespace between HTML tags)
result := removeWhitespaceBetweenTags(content)
-
+
// Write the processed result
_, err := w.Write([]byte(result))
return err
@@ -68,4 +68,4 @@ func removeWhitespaceBetweenTags(content string) string {
// This regex matches whitespace between HTML tags only
re := regexp.MustCompile(">\\s+<")
return re.ReplaceAllString(content, "><")
-}
\ No newline at end of file
+}
diff --git a/whitespace_test.go b/whitespace_test.go
index 3587a66..4218ab7 100644
--- a/whitespace_test.go
+++ b/whitespace_test.go
@@ -4,7 +4,6 @@ import (
"testing"
)
-
func TestWhitespaceControl(t *testing.T) {
testCases := []struct {
name string
@@ -28,7 +27,7 @@ func TestWhitespaceControl(t *testing.T) {
},
{
name: "Block tag whitespace control",
- template: "Hello {%- if true %}Yes{% endif -%} World", // Original test case
+ template: "Hello {%- if true %}Yes{% endif -%} World", // Original test case
expected: "HelloYesWorld",
},
{
@@ -46,22 +45,22 @@ func TestWhitespaceControl(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
engine := New()
-
+
// Register the template
err := engine.RegisterString("test.twig", tc.template)
if err != nil {
t.Fatalf("Failed to register template: %v", err)
}
-
+
// Render with context
result, err := engine.Render("test.twig", map[string]interface{}{
"name": "World",
})
-
+
if err != nil {
t.Fatalf("Failed to render template: %v", err)
}
-
+
if result != tc.expected {
t.Errorf("Expected: %q, got: %q for template: %q", tc.expected, result, tc.template)
}
@@ -111,26 +110,26 @@ func TestSpacelessTag(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
engine := New()
-
+
// Register the template
err := engine.RegisterString("test.twig", tc.template)
if err != nil {
t.Fatalf("Failed to register template: %v", err)
}
-
+
// Render with context
result, err := engine.Render("test.twig", map[string]interface{}{
"greeting": "Hello",
"name": "World",
})
-
+
if err != nil {
t.Fatalf("Failed to render template: %v", err)
}
-
+
if result != tc.expected {
t.Errorf("Expected: %q, got: %q for template: %q", tc.expected, result, tc.template)
}
})
}
-}
\ No newline at end of file
+}