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 }}
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 }}