jet.go 11 KB

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