set.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package jet
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/ioutil"
  6. "path"
  7. "path/filepath"
  8. "reflect"
  9. "sync"
  10. "text/template"
  11. )
  12. // Set is responsible to load, parse and cache templates.
  13. // Every Jet template is associated with a Set.
  14. type Set struct {
  15. loader Loader
  16. cache Cache
  17. escapee SafeWriter // escapee to use at runtime
  18. globals VarMap // global scope for this template set
  19. gmx *sync.RWMutex // global variables map mutex
  20. extensions []string
  21. developmentMode bool
  22. leftDelim string
  23. rightDelim string
  24. }
  25. // Option is the type of option functions that can be used in NewSet().
  26. type Option func(*Set)
  27. // NewSet returns a new Set relying on loader. NewSet panics if a nil Loader is passed.
  28. func NewSet(loader Loader, opts ...Option) *Set {
  29. if loader == nil {
  30. panic(errors.New("jet: NewSet() must not be called with a nil loader"))
  31. }
  32. s := &Set{
  33. loader: loader,
  34. cache: &cache{},
  35. escapee: template.HTMLEscape,
  36. globals: VarMap{},
  37. gmx: &sync.RWMutex{},
  38. extensions: []string{
  39. "", // in case the path is given with the correct extension already
  40. ".jet",
  41. ".html.jet",
  42. ".jet.html",
  43. },
  44. }
  45. for _, opt := range opts {
  46. opt(s)
  47. }
  48. return s
  49. }
  50. // WithCache returns an option function that sets the cache to use for template parsing results.
  51. // Use InDevelopmentMode() to disable caching of parsed templates. By default, Jet uses a
  52. // concurrency-safe in-memory cache that holds templates forever.
  53. func WithCache(c Cache) Option {
  54. if c == nil {
  55. panic(errors.New("jet: WithCache() must not be called with a nil cache"))
  56. }
  57. return func(s *Set) {
  58. s.cache = c
  59. }
  60. }
  61. // WithSafeWriter returns an option function that sets the escaping function to use when executing
  62. // templates. By default, Jet uses a writer that takes care of HTML escaping. Pass nil to disable escaping.
  63. func WithSafeWriter(w SafeWriter) Option {
  64. return func(s *Set) {
  65. s.escapee = w
  66. }
  67. }
  68. // WithDelims returns an option function that sets the delimiters to the specified strings.
  69. // Parsed templates will inherit the settings. Not setting them leaves them at the default: `{{` and `}}`.
  70. func WithDelims(left, right string) Option {
  71. return func(s *Set) {
  72. s.leftDelim = left
  73. s.rightDelim = right
  74. }
  75. }
  76. // WithTemplateNameExtensions returns an option function that sets the extensions to try when looking
  77. // up template names in the cache or loader. Default extensions are `""` (no extension), `".jet"`,
  78. // `".html.jet"`, `".jet.html"`. Extensions will be tried in the order they are defined in the slice.
  79. // WithTemplateNameExtensions panics when you pass in a nil or empty slice.
  80. func WithTemplateNameExtensions(extensions []string) Option {
  81. if len(extensions) == 0 {
  82. panic(errors.New("jet: WithTemplateNameExtensions() must not be called with a nil or empty slice of extensions"))
  83. }
  84. return func(s *Set) {
  85. s.extensions = extensions
  86. }
  87. }
  88. // InDevelopmentMode returns an option function that toggles development mode on, meaning the cache will
  89. // always be bypassed and every template lookup will go to the loader.
  90. func InDevelopmentMode() Option {
  91. return func(s *Set) {
  92. s.developmentMode = true
  93. }
  94. }
  95. // GetTemplate tries to find (and parse, if not yet parsed) the template at the specified path.
  96. //
  97. // For example, GetTemplate("catalog/products.list") with extensions set to []string{"", ".html.jet",".jet"}
  98. // will try to look for:
  99. // 1. catalog/products.list
  100. // 2. catalog/products.list.html.jet
  101. // 3. catalog/products.list.jet
  102. // in the set's templates cache, and if it can't find the template it will try to load the same paths via
  103. // the loader, and, if parsed successfully, cache the template (unless running in development mode).
  104. func (s *Set) GetTemplate(templatePath string) (t *Template, err error) {
  105. return s.getSiblingTemplate(templatePath, "/", true)
  106. }
  107. func (s *Set) getSiblingTemplate(templatePath, siblingPath string, cacheAfterParsing bool) (t *Template, err error) {
  108. templatePath = filepath.ToSlash(templatePath)
  109. siblingPath = filepath.ToSlash(siblingPath)
  110. if !path.IsAbs(templatePath) {
  111. siblingDir := path.Dir(siblingPath)
  112. templatePath = path.Join(siblingDir, templatePath)
  113. }
  114. return s.getTemplate(templatePath, cacheAfterParsing)
  115. }
  116. // same as GetTemplate, but doesn't cache a template when found through the loader.
  117. func (s *Set) getTemplate(templatePath string, cacheAfterParsing bool) (t *Template, err error) {
  118. if !s.developmentMode {
  119. t, found := s.getTemplateFromCache(templatePath)
  120. if found {
  121. return t, nil
  122. }
  123. }
  124. t, err = s.getTemplateFromLoader(templatePath, cacheAfterParsing)
  125. if err == nil && cacheAfterParsing && !s.developmentMode {
  126. s.cache.Put(templatePath, t)
  127. }
  128. return t, err
  129. }
  130. func (s *Set) getTemplateFromCache(templatePath string) (t *Template, ok bool) {
  131. // check path with all possible extensions in cache
  132. for _, extension := range s.extensions {
  133. canonicalPath := templatePath + extension
  134. if t := s.cache.Get(canonicalPath); t != nil {
  135. return t, true
  136. }
  137. }
  138. return nil, false
  139. }
  140. func (s *Set) getTemplateFromLoader(templatePath string, cacheAfterParsing bool) (t *Template, err error) {
  141. // check path with all possible extensions in loader
  142. for _, extension := range s.extensions {
  143. canonicalPath := templatePath + extension
  144. if found := s.loader.Exists(canonicalPath); found {
  145. return s.loadFromFile(canonicalPath, cacheAfterParsing)
  146. }
  147. }
  148. return nil, fmt.Errorf("template %s could not be found", templatePath)
  149. }
  150. func (s *Set) loadFromFile(templatePath string, cacheAfterParsing bool) (template *Template, err error) {
  151. f, err := s.loader.Open(templatePath)
  152. if err != nil {
  153. return nil, err
  154. }
  155. defer f.Close()
  156. content, err := ioutil.ReadAll(f)
  157. if err != nil {
  158. return nil, err
  159. }
  160. return s.parse(templatePath, string(content), cacheAfterParsing)
  161. }
  162. // Parse parses `contents` as if it were located at `templatePath`, but won't put the result into the cache.
  163. // Any referenced template (e.g. via `extends` or `import` statements) will be tried to be loaded from the cache.
  164. // If a referenced template has to be loaded and parsed, it will also not be put into the cache after parsing.
  165. func (s *Set) Parse(templatePath, contents string) (template *Template, err error) {
  166. templatePath = filepath.ToSlash(templatePath)
  167. switch path.Base(templatePath) {
  168. case ".", "/":
  169. return nil, errors.New("template path has no base name")
  170. }
  171. // make sure it's absolute and clean it
  172. templatePath = path.Join("/", templatePath)
  173. return s.parse(templatePath, contents, false)
  174. }
  175. // AddGlobal adds a global variable into the Set,
  176. // overriding any value previously set under the specified key.
  177. // It returns the Set it was called on to allow for method chaining.
  178. func (s *Set) AddGlobal(key string, i interface{}) *Set {
  179. s.gmx.Lock()
  180. defer s.gmx.Unlock()
  181. s.globals[key] = reflect.ValueOf(i)
  182. return s
  183. }
  184. // LookupGlobal returns the global variable previously set under the specified key.
  185. // It returns the nil interface and false if no variable exists under that key.
  186. func (s *Set) LookupGlobal(key string) (val interface{}, found bool) {
  187. s.gmx.RLock()
  188. defer s.gmx.RUnlock()
  189. val, found = s.globals[key]
  190. return
  191. }
  192. // AddGlobalFunc adds a global function into the Set,
  193. // overriding any function previously set under the specified key.
  194. // It returns the Set it was called on to allow for method chaining.
  195. func (s *Set) AddGlobalFunc(key string, fn Func) *Set {
  196. return s.AddGlobal(key, fn)
  197. }