diff --git a/node.go b/node.go index d2e1edb..14d9c11 100644 --- a/node.go +++ b/node.go @@ -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 } } diff --git a/relative_path_test.go b/relative_path_test.go index cc439b7..6fde808 100644 --- a/relative_path_test.go +++ b/relative_path_test.go @@ -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 := ` + + + {% block title %}Main Layout Title{% endblock %} + {% block stylesheet %}{% endblock stylesheet %} + + + {% block content %} + {% endblock content %} + {% block javascript %}{% endblock javascript %} + +` + + // 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 %} + +{% endblock %} + {% block content %} +
+

{{ title }}

+

{{ content }}

+ + main +
+ {% endblock %} + {% block javascript %} + + {% 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, "") { + t.Errorf("Output should contain DOCTYPE declaration from main layout template") + } + + if !strings.Contains(output, "
") { + 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") +}