handlebars.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. package view
  2. import (
  3. "fmt"
  4. "html/template"
  5. "io"
  6. "io/fs"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "github.com/kataras/iris/v12/context"
  12. "github.com/mailgun/raymond/v2"
  13. )
  14. // HandlebarsEngine contains the handlebars view engine structure.
  15. type HandlebarsEngine struct {
  16. fs fs.FS
  17. // files configuration
  18. rootDir string
  19. extension string
  20. // Not used anymore.
  21. // assetFn func(name string) ([]byte, error) // for embedded, in combination with directory & extension
  22. // namesFn func() []string // for embedded, in combination with directory & extension
  23. reload bool // if true, each time the ExecuteWriter is called the templates will be reloaded.
  24. // parser configuration
  25. layout string
  26. rmu sync.RWMutex
  27. funcs template.FuncMap
  28. templateCache map[string]*raymond.Template
  29. }
  30. var (
  31. _ Engine = (*HandlebarsEngine)(nil)
  32. _ EngineFuncer = (*HandlebarsEngine)(nil)
  33. )
  34. // Handlebars creates and returns a new handlebars view engine.
  35. // The given "extension" MUST begin with a dot.
  36. //
  37. // Usage:
  38. // Handlebars("./views", ".html") or
  39. // Handlebars(iris.Dir("./views"), ".html") or
  40. // Handlebars(embed.FS, ".html") or Handlebars(AssetFile(), ".html") for embedded data.
  41. func Handlebars(fs interface{}, extension string) *HandlebarsEngine {
  42. s := &HandlebarsEngine{
  43. fs: getFS(fs),
  44. rootDir: "/",
  45. extension: extension,
  46. templateCache: make(map[string]*raymond.Template),
  47. funcs: make(template.FuncMap), // global
  48. }
  49. // register the render helper here
  50. raymond.RegisterHelper("render", func(partial string, binding interface{}) raymond.SafeString {
  51. contents, err := s.executeTemplateBuf(partial, binding)
  52. if err != nil {
  53. return raymond.SafeString("template with name: " + partial + " couldn't not be found.")
  54. }
  55. return raymond.SafeString(contents)
  56. })
  57. return s
  58. }
  59. // RootDir sets the directory to be used as a starting point
  60. // to load templates from the provided file system.
  61. func (s *HandlebarsEngine) RootDir(root string) *HandlebarsEngine {
  62. if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
  63. sub, err := fs.Sub(s.fs, s.rootDir)
  64. if err != nil {
  65. panic(err)
  66. }
  67. s.fs = sub // here so the "middleware" can work.
  68. }
  69. s.rootDir = filepath.ToSlash(root)
  70. return s
  71. }
  72. // Name returns the handlebars engine's name.
  73. func (s *HandlebarsEngine) Name() string {
  74. return "Handlebars"
  75. }
  76. // Ext returns the file extension which this view engine is responsible to render.
  77. // If the filename extension on ExecuteWriter is empty then this is appended.
  78. func (s *HandlebarsEngine) Ext() string {
  79. return s.extension
  80. }
  81. // Reload if set to true the templates are reloading on each render,
  82. // use it when you're in development and you're boring of restarting
  83. // the whole app when you edit a template file.
  84. //
  85. // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time,
  86. // no concurrent access across clients, use it only on development status.
  87. // It's good to be used side by side with the https://github.com/kataras/rizla reloader for go source files.
  88. func (s *HandlebarsEngine) Reload(developmentMode bool) *HandlebarsEngine {
  89. s.reload = developmentMode
  90. return s
  91. }
  92. // Layout sets the layout template file which should use
  93. // the {{ yield . }} func to yield the main template file
  94. // and optionally {{partial/partial_r/render . }} to render
  95. // other template files like headers and footers.
  96. func (s *HandlebarsEngine) Layout(layoutFile string) *HandlebarsEngine {
  97. s.layout = layoutFile
  98. return s
  99. }
  100. // AddFunc adds a function to the templates.
  101. // It is legal to overwrite elements of the default actions:
  102. // - url func(routeName string, args ...string) string
  103. // - urlpath func(routeName string, args ...string) string
  104. // - render func(fullPartialName string) (raymond.HTML, error).
  105. func (s *HandlebarsEngine) AddFunc(funcName string, funcBody interface{}) {
  106. s.rmu.Lock()
  107. s.funcs[funcName] = funcBody
  108. s.rmu.Unlock()
  109. }
  110. // AddGlobalFunc registers a global template function for all Handlebars view engines.
  111. func (s *HandlebarsEngine) AddGlobalFunc(funcName string, funcBody interface{}) {
  112. s.rmu.Lock()
  113. raymond.RegisterHelper(funcName, funcBody)
  114. s.rmu.Unlock()
  115. }
  116. // Load parses the templates to the engine.
  117. // It is responsible to add the necessary global functions.
  118. //
  119. // Returns an error if something bad happens, user is responsible to catch it.
  120. func (s *HandlebarsEngine) Load() error {
  121. // If only custom templates should be loaded.
  122. if (s.fs == nil || context.IsNoOpFS(s.fs)) && len(s.templateCache) > 0 {
  123. return nil
  124. }
  125. rootDirName := getRootDirName(s.fs)
  126. return walk(s.fs, "", func(path string, info os.FileInfo, _ error) error {
  127. if info == nil || info.IsDir() {
  128. return nil
  129. }
  130. if s.extension != "" {
  131. if !strings.HasSuffix(path, s.extension) {
  132. return nil
  133. }
  134. }
  135. if s.rootDir == rootDirName {
  136. path = strings.TrimPrefix(path, rootDirName)
  137. path = strings.TrimPrefix(path, "/")
  138. }
  139. contents, err := asset(s.fs, path)
  140. if err != nil {
  141. return err
  142. }
  143. return s.ParseTemplate(path, string(contents), nil)
  144. })
  145. }
  146. // ParseTemplate adds a custom template from text.
  147. func (s *HandlebarsEngine) ParseTemplate(name string, contents string, funcs template.FuncMap) error {
  148. s.rmu.Lock()
  149. defer s.rmu.Unlock()
  150. name = strings.TrimPrefix(name, "/")
  151. tmpl, err := raymond.Parse(contents)
  152. if err == nil {
  153. // Add functions for this template.
  154. for k, v := range s.funcs {
  155. tmpl.RegisterHelper(k, v)
  156. }
  157. for k, v := range funcs {
  158. tmpl.RegisterHelper(k, v)
  159. }
  160. s.templateCache[name] = tmpl
  161. }
  162. return err
  163. }
  164. func (s *HandlebarsEngine) fromCache(relativeName string) *raymond.Template {
  165. if s.reload {
  166. s.rmu.RLock()
  167. defer s.rmu.RUnlock()
  168. }
  169. if tmpl, ok := s.templateCache[relativeName]; ok {
  170. return tmpl
  171. }
  172. return nil
  173. }
  174. func (s *HandlebarsEngine) executeTemplateBuf(name string, binding interface{}) (string, error) {
  175. if tmpl := s.fromCache(name); tmpl != nil {
  176. return tmpl.Exec(binding)
  177. }
  178. return "", nil
  179. }
  180. // ExecuteWriter executes a template and writes its result to the w writer.
  181. func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
  182. // re-parse the templates if reload is enabled.
  183. if s.reload {
  184. if err := s.Load(); err != nil {
  185. return err
  186. }
  187. }
  188. isLayout := false
  189. layout = getLayout(layout, s.layout)
  190. renderFilename := filename
  191. if layout != "" {
  192. isLayout = true
  193. renderFilename = layout // the render becomes the layout, and the name is the partial.
  194. }
  195. if tmpl := s.fromCache(renderFilename); tmpl != nil {
  196. binding := bindingData
  197. if isLayout {
  198. var context map[string]interface{}
  199. if m, is := binding.(map[string]interface{}); is { // handlebars accepts maps,
  200. context = m
  201. } else {
  202. return fmt.Errorf("please provide a map[string]interface{} type as the binding instead of the %#v", binding)
  203. }
  204. contents, err := s.executeTemplateBuf(filename, binding)
  205. if err != nil {
  206. return err
  207. }
  208. if context == nil {
  209. context = make(map[string]interface{}, 1)
  210. }
  211. // I'm implemented the {{ yield . }} as with the rest of template engines, so this is not inneed for iris, but the user can do that manually if want
  212. // there is no performance cost: raymond.RegisterPartialTemplate(name, tmpl)
  213. context["yield"] = raymond.SafeString(contents)
  214. }
  215. res, err := tmpl.Exec(binding)
  216. if err != nil {
  217. return err
  218. }
  219. _, err = fmt.Fprint(w, res)
  220. return err
  221. }
  222. return ErrNotExist{
  223. Name: fmt.Sprintf("%s (file: %s)", renderFilename, filename),
  224. IsLayout: false,
  225. Data: bindingData,
  226. }
  227. }