package html
import (
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
core "github.com/gofiber/template"
"github.com/gofiber/utils"
)
// Engine struct
type Engine struct {
core.Engine
// templates
Templates *template.Template
}
// New returns an HTML render engine for Fiber
func New(directory, extension string) *Engine {
engine := &Engine{
Engine: core.Engine{
Left: "{{",
Right: "}}",
Directory: directory,
Extension: extension,
LayoutName: "embed",
Funcmap: make(map[string]interface{}),
},
}
engine.AddFunc(engine.LayoutName, func() error {
return fmt.Errorf("layoutName called unexpectedly")
})
return engine
}
// NewFileSystem returns an HTML render engine for Fiber with file system
func NewFileSystem(fs http.FileSystem, extension string) *Engine {
engine := &Engine{
Engine: core.Engine{
Left: "{{",
Right: "}}",
Directory: "/",
FileSystem: fs,
Extension: extension,
LayoutName: "embed",
Funcmap: make(map[string]interface{}),
},
}
engine.AddFunc(engine.LayoutName, func() error {
return fmt.Errorf("layoutName called unexpectedly")
})
return engine
}
// Load parses the templates to the engine.
func (e *Engine) Load() error {
if e.Loaded {
return nil
}
// race safe
e.Mutex.Lock()
defer e.Mutex.Unlock()
e.Templates = template.New(e.Directory)
// Set template settings
e.Templates.Delims(e.Left, e.Right)
e.Templates.Funcs(e.Funcmap)
walkFn := func(path string, info os.FileInfo, err error) error {
// Return error if exist
if err != nil {
return err
}
// Skip file if it's a directory or has no file info
if info == nil || info.IsDir() {
return nil
}
// Skip file if it does not equal the given template Extension
if len(e.Extension) >= len(path) || path[len(path)-len(e.Extension):] != e.Extension {
return nil
}
// Get the relative file path
// ./views/html/index.tmpl -> index.tmpl
rel, err := filepath.Rel(e.Directory, path)
if err != nil {
return err
}
// Reverse slashes '\' -> '/' and
// partials\footer.tmpl -> partials/footer.tmpl
name := filepath.ToSlash(rel)
// Remove ext from name 'index.tmpl' -> 'index'
name = strings.TrimSuffix(name, e.Extension)
// name = strings.Replace(name, e.Extension, "", -1)
// Read the file
// #gosec G304
buf, err := utils.ReadFile(path, e.FileSystem)
if err != nil {
return err
}
// Create new template associated with the current one
// This enable use to invoke other templates {{ template .. }}
_, err = e.Templates.New(name).Parse(string(buf))
if err != nil {
return err
}
// Debugging
if e.Verbose {
log.Printf("views: parsed template: %s\n", name)
}
return err
}
// notify engine that we parsed all templates
e.Loaded = true
if e.FileSystem != nil {
return utils.Walk(e.FileSystem, e.Directory, walkFn)
}
return filepath.Walk(e.Directory, walkFn)
}
// Render will execute the template name along with the given values.
func (e *Engine) Render(out io.Writer, name string, binding interface{}, layout ...string) error {
if !e.Loaded || e.ShouldReload {
if e.ShouldReload {
e.Loaded = false
}
if err := e.Load(); err != nil {
return err
}
}
tmpl := e.Templates.Lookup(name)
if tmpl == nil {
return fmt.Errorf("render: template %s does not exist", name)
}
render := renderFuncCreate(e, out, binding, *tmpl, nil)
if len(layout) > 0 && layout[0] != "" {
e.Mutex.Lock()
defer e.Mutex.Unlock()
}
// construct a nested render function to embed templates in layouts
for _, layName := range layout {
if layName == "" {
break
}
lay := e.Templates.Lookup(layName)
if lay == nil {
return fmt.Errorf("render: LayoutName %s does not exist", layName)
}
render = renderFuncCreate(e, out, binding, *lay, render)
}
return render()
}
func renderFuncCreate(e *Engine, out io.Writer, binding interface{}, tmpl template.Template, childRenderFunc func() error) func() error {
return func() error {
tmpl.Funcs(map[string]interface{}{
e.LayoutName: childRenderFunc,
})
return tmpl.Execute(out, binding)
}
}