Fix template fallback mechanism to only execute for not found errors

- Modified ExtendsNode, IncludeNode, ImportNode, and FromImportNode to only fallback when specifically dealing with ErrTemplateNotFound
- When a syntax error or other functional error occurs in templates, the real error is now displayed instead of silently falling back to other templates
- Added proper error type checking with errors.Is() to ensure correct fallback behavior
- Improved ignoreMissing behavior in IncludeNode to only apply to not found errors

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
semihalev 2025-03-12 20:47:20 +03:00
commit 529420e7f2
2 changed files with 162 additions and 15 deletions

31
node.go
View file

@ -2,6 +2,7 @@ package twig
import (
"bytes"
"errors"
"fmt"
"io"
"path/filepath"
@ -684,13 +685,14 @@ func (n *ExtendsNode) Render(w io.Writer, ctx *RenderContext) error {
// Load the parent template with resolved path
parentTemplate, err := ctx.engine.Load(resolvedName)
if err != nil {
// If template not found with resolved path, try original name
if resolvedName != templateName {
// Only try the fallback if the template was not found AND the paths are different
if errors.Is(err, ErrTemplateNotFound) && resolvedName != templateName {
parentTemplate, err = ctx.engine.Load(templateName)
if err != nil {
return err
}
} else {
// For any other error (including syntax errors), return immediately
return err
}
}
@ -701,7 +703,7 @@ func (n *ExtendsNode) Render(w io.Writer, ctx *RenderContext) error {
// This ensures the parent template knows it's being extended and preserves our blocks
parentCtx := NewRenderContext(ctx.env, ctx.context, ctx.engine)
parentCtx.extending = true // Flag that the parent is being extended
// Pass along the parent template as lastLoadedTemplate for relative path resolution
parentCtx.lastLoadedTemplate = parentTemplate
@ -793,19 +795,20 @@ func (n *IncludeNode) Render(w io.Writer, ctx *RenderContext) error {
// Load the template with resolved path
template, err := ctx.engine.Load(resolvedName)
if err != nil {
// If template not found with resolved path, try original name
if resolvedName != templateName {
// Only try the fallback if the template was not found AND the paths are different
if errors.Is(err, ErrTemplateNotFound) && resolvedName != templateName {
template, err = ctx.engine.Load(templateName)
if err != nil {
if n.ignoreMissing {
if n.ignoreMissing && errors.Is(err, ErrTemplateNotFound) {
return nil
}
return err
}
} else {
if n.ignoreMissing {
if n.ignoreMissing && errors.Is(err, ErrTemplateNotFound) {
return nil
}
// For any other error (including syntax errors), return immediately
return err
}
}
@ -818,7 +821,7 @@ func (n *IncludeNode) Render(w io.Writer, ctx *RenderContext) error {
includeCtx := ctx.Clone()
includeCtx.lastLoadedTemplate = template
defer includeCtx.Release()
return template.nodes.Render(w, includeCtx)
}
@ -840,7 +843,7 @@ func (n *IncludeNode) Render(w io.Writer, ctx *RenderContext) error {
// Create a new context
includeCtx = NewRenderContext(ctx.env, contextVars, ctx.engine)
// Set the template as the lastLoadedTemplate for relative path resolutionn includeCtx.lastLoadedTemplate = template
// Set the template as the lastLoadedTemplate for relative path resolutionn includeCtx.lastLoadedTemplate = template
defer includeCtx.Release()
// If sandboxed, enable sandbox mode
@ -1206,13 +1209,14 @@ func (n *ImportNode) Render(w io.Writer, ctx *RenderContext) error {
// Load the template with resolved path
template, err := ctx.engine.Load(resolvedName)
if err != nil {
// If template not found with resolved path, try original name
if resolvedName != templateName {
// Only try the fallback if the template was not found AND the paths are different
if errors.Is(err, ErrTemplateNotFound) && resolvedName != templateName {
template, err = ctx.engine.Load(templateName)
if err != nil {
return err
}
} else {
// For any other error (including syntax errors), return immediately
return err
}
}
@ -1296,13 +1300,14 @@ func (n *FromImportNode) Render(w io.Writer, ctx *RenderContext) error {
// Load the template with resolved path
template, err := ctx.engine.Load(resolvedName)
if err != nil {
// If template not found with resolved path, try original name
if resolvedName != templateName {
// Only try the fallback if the template was not found AND the paths are different
if errors.Is(err, ErrTemplateNotFound) && resolvedName != templateName {
template, err = ctx.engine.Load(templateName)
if err != nil {
return err
}
} else {
// For any other error (including syntax errors), return immediately
return err
}
}

View file

@ -80,10 +80,10 @@ func TestRelativePathsWithFromImport(t *testing.T) {
// Create simple macro templates
macrosTemplate := `{% macro simple() %}Macro output{% endmacro %}`
// Print the template content for debugging
t.Logf("Simple template content: %s", macrosTemplate)
// Note: The template needs to be in the format: {% from "template" import macro %}
useTemplate := `{% from "./simple.twig" import simple %}{{ simple() }}`
t.Logf("Use template content: %s", useTemplate)
@ -129,3 +129,145 @@ func normalizeWhitespace(s string) string {
result := strings.Join(strings.Fields(s), " ")
return result
}
// TestRelativePathsWithExtendsInSubfolder tests a specific scenario where a template
// in a child folder extends another template in the same folder using a relative path
func TestRelativePathsWithExtendsInSubfolder(t *testing.T) {
// Create temporary directories for testing
tempDir, err := os.MkdirTemp("", "twig-test-")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
// Create a main templates directory and a child subdirectory
templatesDir := filepath.Join(tempDir, "templates")
err = os.Mkdir(templatesDir, 0755)
if err != nil {
t.Fatalf("Failed to create templates dir: %v", err)
}
t.Logf("Template dir: %s", templatesDir)
childDir := filepath.Join(templatesDir, "child")
err = os.Mkdir(childDir, 0755)
if err != nil {
t.Fatalf("Failed to create child dir: %v", err)
}
// Create the layout template in the templates directory
layoutTemplate := `<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Main Layout Title{% endblock %}</title>
{% block stylesheet %}{% endblock stylesheet %}
</head>
<body>
{% block content %}
{% endblock content %}
{% block javascript %}{% endblock javascript %}
</body>
</html>`
// Create the base template in the child directory
baseTemplate := `{% extends '../layout.html.twig' %}
{% block title %}{{ title }}{% if pair is defined %} | {{ pair | split('.', 2) | first | capitalize }} | {{ pair | split('.', 2) | last | replace('something', '') }}{% endif %} | Text{% endblock %}
{% block stylesheet %}
<style>
.brand-image {
margin-top: -.5rem;
margin-right: .2rem;
height: 33px;
}
</style>
{% endblock %}
{% block content %}
<div class="container">
<h1>{{ title }}</h1>
<p>{{ content }}</p>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
<a href="{{ '/' }}">main</a>
</div>
{% endblock %}
{% block javascript %}
<script>
console.log('{{ content | split(' ', 2) | first }}');
{% set break = false %}
{% for item in items %}
{% if not break %}
{% set list = item %}
window.top.location = '/{{ list }}';
{% set break = true %}
{% endif %}
{% endfor %}
</script>
{% endblock javascript %}
`
// Create the child template that extends the base using a relative path
childTemplate := `{% extends './layout.html.twig' %}`
// Write templates to files
err = os.WriteFile(filepath.Join(templatesDir, "layout.html.twig"), []byte(layoutTemplate), 0644)
if err != nil {
t.Fatalf("Failed to write base template: %v", err)
}
err = os.WriteFile(filepath.Join(childDir, "layout.html.twig"), []byte(baseTemplate), 0644)
if err != nil {
t.Fatalf("Failed to write base template: %v", err)
}
err = os.WriteFile(filepath.Join(childDir, "child.html.twig"), []byte(childTemplate), 0644)
if err != nil {
t.Fatalf("Failed to write child template: %v", err)
}
// Create a new Twig engine with debug enabled
engine := New()
engine.SetDevelopmentMode(true)
engine.SetDebug(true)
engine.SetAutoReload(true)
// Register the template directory
loader := NewFileSystemLoader([]string{templatesDir})
engine.RegisterLoader(loader)
// Render the child template from the child directory
output, err := engine.Render("child/child.html.twig", map[string]interface{}{
"title": "Base Title",
"content": "This is the base content.",
"items": []string{"Item 1", "Item 2", "Item 3"},
})
if err != nil {
t.Fatalf("Failed to render template: %v", err)
}
//t.Logf("Output: %s", output)
// Verify the output contains elements from both templates
if !strings.Contains(output, "Base Title") {
t.Errorf("Output should contain 'Base Title' from base template title block")
}
if !strings.Contains(output, "<!DOCTYPE html>") {
t.Errorf("Output should contain DOCTYPE declaration from main layout template")
}
if !strings.Contains(output, "<div class=\"container\">") {
t.Errorf("Output should contain container div from base template template")
}
// The child content should contain the base content
if !strings.Contains(output, "This is the base content") {
t.Errorf("Output should contain 'This is the base content' from base template")
}
t.Logf("Successfully rendered template with relative extends path in subfolder")
}