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 }