jet.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. package view
  2. import (
  3. "fmt"
  4. "io"
  5. "os"
  6. "path"
  7. "path/filepath"
  8. "reflect"
  9. "strings"
  10. "github.com/kataras/iris/context"
  11. "github.com/CloudyKit/jet/v4"
  12. )
  13. const jetEngineName = "jet"
  14. // JetEngine is the jet template parser's view engine.
  15. type JetEngine struct {
  16. directory string
  17. extension string
  18. // physical system files or app-embedded, see `Binary(..., ...)`. Defaults to file system on initialization.
  19. loader jet.Loader
  20. developmentMode bool
  21. // The Set is the `*jet.Set`, exported to offer any custom capabilities that jet users may want.
  22. // Available after `Load`.
  23. Set *jet.Set
  24. // Note that global vars and functions are set in a single spot on the jet parser.
  25. // If AddFunc or AddVar called before `Load` then these will be set here to be used via `Load` and clear.
  26. vars map[string]interface{}
  27. jetDataContextKey string
  28. }
  29. var (
  30. _ Engine = (*JetEngine)(nil)
  31. _ EngineFuncer = (*JetEngine)(nil)
  32. )
  33. // jet library does not export or give us any option to modify them via Set
  34. // (unless we parse the files by ourselves but this is not a smart choice).
  35. var jetExtensions = [...]string{
  36. ".html.jet",
  37. ".jet.html",
  38. ".jet",
  39. }
  40. // Jet creates and returns a new jet view engine.
  41. // The given "extension" MUST begin with a dot.
  42. func Jet(directory, extension string) *JetEngine {
  43. // if _, err := os.Stat(directory); os.IsNotExist(err) {
  44. // panic(err)
  45. // }
  46. extOK := false
  47. for _, ext := range jetExtensions {
  48. if ext == extension {
  49. extOK = true
  50. break
  51. }
  52. }
  53. if !extOK {
  54. panic(fmt.Sprintf("%s extension is not a valid jet engine extension[%s]", extension, strings.Join(jetExtensions[0:], ", ")))
  55. }
  56. s := &JetEngine{
  57. directory: directory,
  58. extension: extension,
  59. loader: jet.NewOSFileSystemLoader(directory),
  60. jetDataContextKey: "_jet",
  61. }
  62. return s
  63. }
  64. // String returns the name of this view engine, the "jet".
  65. func (s *JetEngine) String() string {
  66. return jetEngineName
  67. }
  68. // Ext should return the final file extension which this view engine is responsible to render.
  69. func (s *JetEngine) Ext() string {
  70. return s.extension
  71. }
  72. // Delims sets the action delimiters to the specified strings, to be used in
  73. // templates. An empty delimiter stands for the
  74. // corresponding default: {{ or }}.
  75. // Should act before `Load` or `iris.Application#RegisterView`.
  76. func (s *JetEngine) Delims(left, right string) *JetEngine {
  77. s.Set.Delims(left, right)
  78. return s
  79. }
  80. // JetArguments is a type alias of `jet.Arguments`,
  81. // can be used on `AddFunc$funcBody`.
  82. type JetArguments = jet.Arguments
  83. // AddFunc should adds a global function to the jet template set.
  84. func (s *JetEngine) AddFunc(funcName string, funcBody interface{}) {
  85. // if something like "urlpath" is registered.
  86. if generalFunc, ok := funcBody.(func(string, ...interface{}) string); ok {
  87. // jet, unlike others does not accept a func(string, ...interface{}) string,
  88. // instead it wants:
  89. // func(JetArguments) reflect.Value.
  90. s.AddVar(funcName, jet.Func(func(args JetArguments) reflect.Value {
  91. n := args.NumOfArguments()
  92. if n == 0 { // no input, don't execute the function, panic instead.
  93. panic(funcName + " expects one or more input arguments")
  94. }
  95. firstInput := args.Get(0).String()
  96. if n == 1 { // if only the first argument is given.
  97. return reflect.ValueOf(generalFunc(firstInput))
  98. }
  99. // if has variadic.
  100. variadicN := n - 1
  101. variadicInputs := make([]interface{}, variadicN) // except the first one.
  102. for i := 0; i < variadicN; i++ {
  103. variadicInputs[i] = args.Get(i + 1).Interface()
  104. }
  105. return reflect.ValueOf(generalFunc(firstInput, variadicInputs...))
  106. }))
  107. return
  108. }
  109. if jetFunc, ok := funcBody.(jet.Func); !ok {
  110. alternativeJetFunc, ok := funcBody.(func(JetArguments) reflect.Value)
  111. if !ok {
  112. panic(fmt.Sprintf("JetEngine.AddFunc: funcBody argument is not a type of func(JetArguments) reflect.Value. Got %T instead", funcBody))
  113. }
  114. s.AddVar(funcName, jet.Func(alternativeJetFunc))
  115. } else {
  116. s.AddVar(funcName, jetFunc)
  117. }
  118. }
  119. // AddVar adds a global variable to the jet template set.
  120. func (s *JetEngine) AddVar(key string, value interface{}) {
  121. if s.Set != nil {
  122. s.Set.AddGlobal(key, value)
  123. } else {
  124. if s.vars == nil {
  125. s.vars = make(map[string]interface{})
  126. }
  127. s.vars[key] = value
  128. }
  129. }
  130. // Reload if setted to true the templates are reloading on each render,
  131. // use it when you're in development and you're boring of restarting
  132. // the whole app when you edit a template file.
  133. //
  134. // Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time,
  135. // not safe concurrent access across clients, use it only on development state.
  136. func (s *JetEngine) Reload(developmentMode bool) *JetEngine {
  137. s.developmentMode = developmentMode
  138. if s.Set != nil {
  139. s.Set.SetDevelopmentMode(developmentMode)
  140. }
  141. return s
  142. }
  143. // SetLoader can be used when the caller wants to use something like
  144. // multi.Loader or httpfs.Loader of the jet subpackages,
  145. // overrides any previous loader may set by `Binary` or the default.
  146. // Should act before `Load` or `iris.Application#RegisterView`.
  147. func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine {
  148. s.loader = loader
  149. return s
  150. }
  151. // Binary optionally, use it when template files are distributed
  152. // inside the app executable (.go generated files).
  153. //
  154. // The assetFn and namesFn can come from the go-bindata library.
  155. // Should act before `Load` or `iris.Application#RegisterView`.
  156. func (s *JetEngine) Binary(assetFn func(name string) ([]byte, error), assetNames func() []string) *JetEngine {
  157. // embedded.
  158. vdir := s.directory
  159. if vdir[0] == '.' {
  160. vdir = vdir[1:]
  161. }
  162. // second check for /something, (or ./something if we had dot on 0 it will be removed)
  163. if vdir[0] == '/' || vdir[0] == os.PathSeparator {
  164. vdir = vdir[1:]
  165. }
  166. // check for trailing slashes because new users may be do that by mistake
  167. // although all examples are showing the correct way but you never know
  168. // i.e "./assets/" is not correct, if was inside "./assets".
  169. // remove last "/".
  170. if trailingSlashIdx := len(vdir) - 1; vdir[trailingSlashIdx] == '/' {
  171. vdir = vdir[0:trailingSlashIdx]
  172. }
  173. namesSlice := assetNames()
  174. names := make(map[string]struct{})
  175. for _, name := range namesSlice {
  176. if !strings.HasPrefix(name, vdir) {
  177. continue
  178. }
  179. extOK := false
  180. fileExt := path.Ext(name)
  181. for _, ext := range jetExtensions {
  182. if ext == fileExt {
  183. extOK = true
  184. break
  185. }
  186. }
  187. if !extOK {
  188. continue
  189. }
  190. names[name] = struct{}{}
  191. }
  192. if len(names) == 0 {
  193. panic("JetEngine.Binary: no embedded files found in directory: " + vdir)
  194. }
  195. s.loader = &embeddedLoader{
  196. vdir: vdir,
  197. asset: assetFn,
  198. names: names,
  199. }
  200. return s
  201. }
  202. type (
  203. embeddedLoader struct {
  204. vdir string
  205. asset func(name string) ([]byte, error)
  206. names map[string]struct{}
  207. }
  208. embeddedFile struct {
  209. contents []byte // the contents are NOT consumed.
  210. readen int64
  211. }
  212. )
  213. var (
  214. _ jet.Loader = (*embeddedLoader)(nil)
  215. _ io.ReadCloser = (*embeddedFile)(nil)
  216. )
  217. func (f *embeddedFile) Close() error { return nil }
  218. func (f *embeddedFile) Read(p []byte) (int, error) {
  219. if f.readen >= int64(len(f.contents)) {
  220. return 0, io.EOF
  221. }
  222. n := copy(p, f.contents[f.readen:])
  223. f.readen += int64(n)
  224. return n, nil
  225. }
  226. // Open opens a file from OS file system.
  227. func (l *embeddedLoader) Open(name string) (io.ReadCloser, error) {
  228. name = path.Join(l.vdir, filepath.ToSlash(name))
  229. contents, err := l.asset(name)
  230. if err != nil {
  231. return nil, err
  232. }
  233. return &embeddedFile{
  234. contents: contents,
  235. }, nil
  236. }
  237. // Exists checks if the template name exists by walking the list of template paths
  238. // returns string with the full path of the template and bool true if the template file was found
  239. func (l *embeddedLoader) Exists(name string) (string, bool) {
  240. name = path.Join(l.vdir, filepath.ToSlash(name))
  241. if _, ok := l.names[name]; ok {
  242. return name, true
  243. }
  244. return "", false
  245. }
  246. // Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
  247. func (s *JetEngine) Load() error {
  248. s.Set = jet.NewHTMLSetLoader(s.loader)
  249. s.Set.SetDevelopmentMode(s.developmentMode)
  250. if s.vars != nil {
  251. for key, value := range s.vars {
  252. s.Set.AddGlobal(key, value)
  253. }
  254. }
  255. // Note that, unlike the rest of template engines implementations,
  256. // we don't call the Set.GetTemplate to parse the templates,
  257. // we let it to the jet template parser itself which does that at serve-time and caches each template by itself.
  258. return nil
  259. }
  260. type (
  261. // JetRuntimeVars is a type alias for `jet.VarMap`.
  262. // Can be used at `AddJetRuntimeVars/JetEngine.AddRuntimeVars`
  263. // to set a runtime variable ${name} to the executing template.
  264. JetRuntimeVars = jet.VarMap
  265. // JetRuntime is a type alias of `jet.Runtime`,
  266. // can be used on RuntimeVariable input function.
  267. JetRuntime = jet.Runtime
  268. )
  269. // JetRuntimeVarsContextKey is the Iris Context key to keep any custom jet runtime variables.
  270. // See `AddJetRuntimeVars` package-level function and `JetEngine.AddRuntimeVars` method.
  271. const JetRuntimeVarsContextKey = "iris.jetvarmap"
  272. // AddJetRuntimeVars sets or inserts runtime jet variables through the Iris Context.
  273. // This gives the ability to add runtime variables from different handlers in the request chain,
  274. // something that the jet template parser does not offer at all.
  275. //
  276. // Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}).
  277. // See `JetEngine.AddRuntimeVars` too.
  278. func AddJetRuntimeVars(ctx *context.Context, jetVarMap JetRuntimeVars) {
  279. if v := ctx.Values().Get(JetRuntimeVarsContextKey); v != nil {
  280. if vars, ok := v.(JetRuntimeVars); ok {
  281. for key, value := range jetVarMap {
  282. vars[key] = value
  283. }
  284. return
  285. }
  286. }
  287. ctx.Values().Set(JetRuntimeVarsContextKey, jetVarMap)
  288. }
  289. // AddRuntimeVars sets or inserts runtime jet variables through the Iris Context.
  290. // This gives the ability to add runtime variables from different handlers in the request chain,
  291. // something that the jet template parser does not offer at all.
  292. //
  293. // Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}).
  294. // See `view.AddJetRuntimeVars` if package-level access is more meanful to the code flow.
  295. func (s *JetEngine) AddRuntimeVars(ctx *context.Context, vars JetRuntimeVars) {
  296. AddJetRuntimeVars(ctx, vars)
  297. }
  298. // ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
  299. func (s *JetEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
  300. tmpl, err := s.Set.GetTemplate(filename)
  301. if err != nil {
  302. return err
  303. }
  304. var vars JetRuntimeVars
  305. if ctx, ok := w.(*context.Context); ok {
  306. runtimeVars := ctx.Values().Get(JetRuntimeVarsContextKey)
  307. if runtimeVars != nil {
  308. if jetVars, ok := runtimeVars.(JetRuntimeVars); ok {
  309. vars = jetVars
  310. }
  311. }
  312. if v := ctx.Values().Get(s.jetDataContextKey); v != nil {
  313. if bindingData == nil {
  314. // if bindingData is nil, try to fill them by context key (a middleware can set data).
  315. bindingData = v
  316. } else if m, ok := bindingData.(context.Map); ok {
  317. // else if bindingData are passed to App/Context.View
  318. // and it's map try to fill with the new values passed from a middleware.
  319. if mv, ok := v.(context.Map); ok {
  320. for key, value := range mv {
  321. m[key] = value
  322. }
  323. }
  324. }
  325. }
  326. }
  327. if bindingData == nil {
  328. return tmpl.Execute(w, vars, nil)
  329. }
  330. if vars == nil {
  331. vars = make(JetRuntimeVars)
  332. }
  333. /* fixed on jet v4.0.0, so no need of this:
  334. if m, ok := bindingData.(context.Map); ok {
  335. var jetData interface{}
  336. for k, v := range m {
  337. if k == s.jetDataContextKey {
  338. jetData = v
  339. continue
  340. }
  341. if value, ok := v.(reflect.Value); ok {
  342. vars[k] = value
  343. } else {
  344. vars[k] = reflect.ValueOf(v)
  345. }
  346. }
  347. if jetData != nil {
  348. bindingData = jetData
  349. }
  350. }*/
  351. return tmpl.Execute(w, vars, bindingData)
  352. }