/ web / engine.go
engine.go
  1  package web
  2  
  3  import (
  4  	"fmt"
  5  	"html"
  6  	"io"
  7  	"net/http"
  8  	"os"
  9  	"path/filepath"
 10  	"strings"
 11  	"sync"
 12  	"text/template"
 13  	"time"
 14  
 15  	"github.com/gofiber/template/utils"
 16  )
 17  
 18  // Engine struct
 19  type Engine struct {
 20  	// delimiters
 21  	left  string
 22  	right string
 23  	// views folder
 24  	directory string
 25  	// http.FileSystem supports embedded files
 26  	fileSystem http.FileSystem
 27  	// views extension
 28  	extension string
 29  	// layout variable name that incapsulates the template
 30  	layout string
 31  	// determines if the engine parsed all templates
 32  	loaded bool
 33  	// reload on each render
 34  	reload bool
 35  	// debug prints the parsed templates
 36  	debug bool
 37  	// lock for funcmap and templates
 38  	mutex sync.RWMutex
 39  	// template funcmap
 40  	funcmap map[string]interface{}
 41  	// templates
 42  	Templates *template.Template
 43  }
 44  
 45  // New returns a HTML render engine for Fiber
 46  func New(directory, extension string) *Engine {
 47  	engine := &Engine{
 48  		left:      "{{",
 49  		right:     "}}",
 50  		directory: directory,
 51  		extension: extension,
 52  		layout:    "embed",
 53  		funcmap:   make(map[string]interface{}),
 54  	}
 55  	engine.AddFunc(engine.layout, func() error {
 56  		return fmt.Errorf("layout called unexpectedly.")
 57  	})
 58  	engine.AddFunc("escape", func(s string) string {
 59  		return html.EscapeString(s)
 60  	})
 61  	engine.AddFunc("timestamp", func(t time.Time) string {
 62  		return t.Format(time.RFC822Z)
 63  	})
 64  
 65  	return engine
 66  }
 67  
 68  //NewFileSystem ...
 69  func NewFileSystem(fs http.FileSystem, extension string) *Engine {
 70  	engine := &Engine{
 71  		left:       "{{",
 72  		right:      "}}",
 73  		directory:  "/",
 74  		fileSystem: fs,
 75  		extension:  extension,
 76  		layout:     "embed",
 77  		funcmap:    make(map[string]interface{}),
 78  	}
 79  	engine.AddFunc(engine.layout, func() error {
 80  		return fmt.Errorf("layout called unexpectedly.")
 81  	})
 82  	engine.AddFunc("escape", func(s string) string {
 83  		return html.EscapeString(s)
 84  	})
 85  	engine.AddFunc("timestamp", func(t time.Time) string {
 86  		return t.Format(time.RFC822Z)
 87  	})
 88  
 89  	return engine
 90  }
 91  
 92  // Layout defines the variable name that will incapsulate the template
 93  func (e *Engine) Layout(key string) *Engine {
 94  	e.layout = key
 95  	return e
 96  }
 97  
 98  // Delims sets the action delimiters to the specified strings, to be used in
 99  // templates. An empty delimiter stands for the
100  // corresponding default: {{ or }}.
101  func (e *Engine) Delims(left, right string) *Engine {
102  	e.left, e.right = left, right
103  	return e
104  }
105  
106  // AddFunc adds the function to the template's function map.
107  // It is legal to overwrite elements of the default actions
108  func (e *Engine) AddFunc(name string, fn interface{}) *Engine {
109  	e.mutex.Lock()
110  	e.funcmap[name] = fn
111  	e.mutex.Unlock()
112  	return e
113  }
114  
115  // AddFuncMap adds the functions from a map to the template's function map.
116  // It is legal to overwrite elements of the default actions
117  func (e *Engine) AddFuncMap(m map[string]interface{}) *Engine {
118  	e.mutex.Lock()
119  	for name, fn := range m {
120  		e.funcmap[name] = fn
121  	}
122  	e.mutex.Unlock()
123  	return e
124  }
125  
126  // Reload if set to true the templates are reloading on each render,
127  // use it when you're in development and you don't want to restart
128  // the application when you edit a template file.
129  func (e *Engine) Reload(enabled bool) *Engine {
130  	e.reload = enabled
131  	return e
132  }
133  
134  // Debug will print the parsed templates when Load is triggered.
135  func (e *Engine) Debug(enabled bool) *Engine {
136  	e.debug = enabled
137  	return e
138  }
139  
140  // Parse is deprecated, please use Load() instead
141  func (e *Engine) Parse() error {
142  	fmt.Println("Parse() is deprecated, please use Load() instead.")
143  	return e.Load()
144  }
145  
146  // Load parses the templates to the engine.
147  func (e *Engine) Load() error {
148  	if e.loaded {
149  		return nil
150  	}
151  	// race safe
152  	e.mutex.Lock()
153  	defer e.mutex.Unlock()
154  	e.Templates = template.New(e.directory)
155  
156  	// Set template settings
157  	e.Templates.Delims(e.left, e.right)
158  	e.Templates.Funcs(e.funcmap)
159  
160  	walkFn := func(path string, info os.FileInfo, err error) error {
161  		// Return error if exist
162  		if err != nil {
163  			return err
164  		}
165  		// Skip file if it's a directory or has no file info
166  		if info == nil || info.IsDir() {
167  			return nil
168  		}
169  		// Skip file if it does not equal the given template extension
170  		if len(e.extension) >= len(path) || path[len(path)-len(e.extension):] != e.extension {
171  			return nil
172  		}
173  		// Get the relative file path
174  		// ./views/html/index.tmpl -> index.tmpl
175  		rel, err := filepath.Rel(e.directory, path)
176  		if err != nil {
177  			return err
178  		}
179  		// Reverse slashes '\' -> '/' and
180  		// partials\footer.tmpl -> partials/footer.tmpl
181  		name := filepath.ToSlash(rel)
182  		// Remove ext from name 'index.tmpl' -> 'index'
183  		name = strings.TrimSuffix(name, e.extension)
184  		// name = strings.Replace(name, e.extension, "", -1)
185  		// Read the file
186  		// #gosec G304
187  		buf, err := utils.ReadFile(path, e.fileSystem)
188  		if err != nil {
189  			return err
190  		}
191  		// Create new template associated with the current one
192  		// This enable use to invoke other templates {{ template .. }}
193  		_, err = e.Templates.New(name).Parse(string(buf))
194  		if err != nil {
195  			return err
196  		}
197  		// Debugging
198  		if e.debug {
199  			fmt.Printf("views: parsed template: %s\n", name)
200  		}
201  		return err
202  	}
203  	// notify engine that we parsed all templates
204  	e.loaded = true
205  	if e.fileSystem != nil {
206  		return utils.Walk(e.fileSystem, e.directory, walkFn)
207  	}
208  	return filepath.Walk(e.directory, walkFn)
209  }
210  
211  // Render will execute the template name along with the given values.
212  func (e *Engine) Render(out io.Writer, template string, binding interface{}, layout ...string) error {
213  	if !e.loaded || e.reload {
214  		if e.reload {
215  			e.loaded = false
216  		}
217  		if err := e.Load(); err != nil {
218  			return err
219  		}
220  	}
221  
222  	tmpl := e.Templates.Lookup(template)
223  	if tmpl == nil {
224  		return fmt.Errorf("render: template %s does not exist", template)
225  	}
226  	if len(layout) > 0 && layout[0] != "" {
227  		lay := e.Templates.Lookup(layout[0])
228  		if lay == nil {
229  			return fmt.Errorf("render: layout %s does not exist", layout[0])
230  		}
231  		e.mutex.Lock()
232  		defer e.mutex.Unlock()
233  		lay.Funcs(map[string]interface{}{
234  			e.layout: func() error {
235  				return tmpl.Execute(out, binding)
236  			},
237  		})
238  		return lay.Execute(out, binding)
239  	}
240  	return tmpl.Execute(out, binding)
241  }