From 6f81cbdc829c9e561ebcd28918f2f8cbae6e72c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 9 Feb 2026 19:40:30 +0100 Subject: [PATCH 1/2] refactor: update partials resolution to support relative paths and improve error handling --- mustache/README.md | 10 ++--- mustache/mustache.go | 76 +++++++++++++++++++++++++++++++++-- mustache/mustache_test.go | 18 ++++++++- mustache/views/index.mustache | 4 +- 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/mustache/README.md b/mustache/README.md index 3aeea553..54ef9a58 100644 --- a/mustache/README.md +++ b/mustache/README.md @@ -21,11 +21,11 @@ go get github.com/gofiber/template/mustache/v4 _**./views/index.mustache**_ ```html -{{> views/partials/header }} +{{> partials/header }}

{{Title}}

-{{> views/partials/footer }} +{{> partials/footer }} ``` _**./views/partials/header.mustache**_ ```html @@ -66,9 +66,9 @@ func main() { engine := mustache.New("./views", ".mustache") // Or from an embedded system - // Note that with an embedded system the partials included from template files must be - // specified relative to the filesystem's root, not the current working directory - // engine := mustache.NewFileSystem(http.Dir("./views", ".mustache"), ".mustache") + // Partials are resolved relative to the engine directory / filesystem root. + // For compatibility, full paths also work when present in your templates. + // engine := mustache.NewFileSystem(http.Dir("./views"), ".mustache") // Pass the engine to the Views app := fiber.New(fiber.Config{ diff --git a/mustache/mustache.go b/mustache/mustache.go index d81adb73..ed214dda 100644 --- a/mustache/mustache.go +++ b/mustache/mustache.go @@ -6,6 +6,7 @@ import ( "log" "net/http" "os" + "path" "path/filepath" "strings" @@ -26,16 +27,81 @@ type Engine struct { type fileSystemPartialProvider struct { fileSystem http.FileSystem extension string + baseDir string + verbose bool } -func (p fileSystemPartialProvider) Get(path string) (string, error) { - buf, err := core.ReadFile(path+p.extension, p.fileSystem) - return string(buf), err +func (p fileSystemPartialProvider) Get(partial string) (string, error) { + candidates := p.lookupCandidates(partial) + var firstErr error + for _, candidate := range candidates { + buf, err := core.ReadFile(candidate, p.fileSystem) + if err == nil { + return string(buf), nil + } + + if firstErr == nil { + firstErr = err + } + if p.verbose { + log.Printf("views: partial lookup failed: partial=%q candidate=%q err=%v", partial, candidate, err) + } + } + + if p.verbose { + log.Printf("views: partial not found: partial=%q candidates=%v", partial, candidates) + } + + if firstErr == nil { + firstErr = fmt.Errorf("no partial candidates generated") + } + return "", fmt.Errorf("render: partial %q does not exist (tried: %s): %w", partial, strings.Join(candidates, ", "), firstErr) +} + +func (p fileSystemPartialProvider) lookupCandidates(partial string) []string { + addCandidate := func(candidates []string, candidate string) []string { + for _, existing := range candidates { + if existing == candidate { + return candidates + } + } + return append(candidates, candidate) + } + + addExtension := func(raw string) string { + if strings.HasSuffix(raw, p.extension) { + return raw + } + return raw + p.extension + } + + base := filepath.ToSlash(strings.TrimSpace(p.baseDir)) + base = strings.TrimSuffix(base, "/") + if base == "." || base == "/" { + base = "" + } + + clean := filepath.ToSlash(strings.TrimSpace(partial)) + clean = strings.TrimPrefix(clean, "./") + + candidates := make([]string, 0, 2) + if clean != "" { + candidates = addCandidate(candidates, addExtension(clean)) + if base != "" { + candidates = addCandidate(candidates, addExtension(path.Join(base, clean))) + } + } + + return candidates } // New returns a Mustache render engine for Fiber func New(directory, extension string) *Engine { engine := &Engine{ + partialsProvider: &fileSystemPartialProvider{ + extension: extension, + baseDir: directory, + }, Engine: core.Engine{ Directory: directory, Extension: extension, @@ -56,6 +122,7 @@ func NewFileSystemPartials(fs http.FileSystem, extension string, partialsFS http partialsProvider: &fileSystemPartialProvider{ fileSystem: partialsFS, extension: extension, + baseDir: "/", }, Engine: core.Engine{ Directory: "/", @@ -75,6 +142,9 @@ func (e *Engine) Load() error { defer e.Mutex.Unlock() e.Templates = make(map[string]*mustache.Template) + if e.partialsProvider != nil { + e.partialsProvider.verbose = e.Verbose + } // Loop trough each directory and register template files walkFn := func(path string, info os.FileInfo, err error) error { diff --git a/mustache/mustache_test.go b/mustache/mustache_test.go index be45c2fd..ac4ab9e5 100644 --- a/mustache/mustache_test.go +++ b/mustache/mustache_test.go @@ -48,6 +48,22 @@ func Test_Render(t *testing.T) { require.Equal(t, expect, result) } +func Test_Render_RelativePartials(t *testing.T) { + t.Parallel() + engine := New("./views", ".mustache") + require.NoError(t, engine.Load()) + + var buf bytes.Buffer + err := engine.Render(&buf, "relative", customMap{ + "Title": "Hello, Relative!", + }) + require.NoError(t, err) + + expect := `

Header

Hello, Relative!

Footer

` + result := trim(buf.String()) + require.Equal(t, expect, result) +} + func Test_Layout(t *testing.T) { t.Parallel() engine := New("./views", ".mustache") @@ -82,7 +98,7 @@ func Test_Empty_Layout(t *testing.T) { func Test_FileSystem(t *testing.T) { t.Parallel() - engine := NewFileSystemPartials(http.Dir("./views"), ".mustache", http.Dir(".")) + engine := NewFileSystemPartials(http.Dir("./views"), ".mustache", http.Dir("./views")) require.NoError(t, engine.Load()) var buf bytes.Buffer diff --git a/mustache/views/index.mustache b/mustache/views/index.mustache index 98615664..bef0b9bd 100644 --- a/mustache/views/index.mustache +++ b/mustache/views/index.mustache @@ -1,3 +1,3 @@ -{{> views/partials/header }} +{{> partials/header }}

{{Title}}

-{{> views/partials/footer }} \ No newline at end of file +{{> partials/footer }} From f1924368d23471109945675ab2e4a737e00f751b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Mon, 9 Feb 2026 19:40:50 +0100 Subject: [PATCH 2/2] refactor: update partials resolution to support relative paths and improve error handling --- mustache/views/relative.mustache | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 mustache/views/relative.mustache diff --git a/mustache/views/relative.mustache b/mustache/views/relative.mustache new file mode 100644 index 00000000..bef0b9bd --- /dev/null +++ b/mustache/views/relative.mustache @@ -0,0 +1,3 @@ +{{> partials/header }} +

{{Title}}

+{{> partials/footer }}