2019-06-22 20:34:19 +02:00
|
|
|
package view
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2022-09-25 19:40:56 +02:00
|
|
|
"io/fs"
|
2020-10-01 15:06:16 +02:00
|
|
|
"os"
|
2020-07-06 20:40:40 +02:00
|
|
|
"path/filepath"
|
2019-06-22 20:34:19 +02:00
|
|
|
"reflect"
|
|
|
|
"strings"
|
2020-09-08 06:55:33 +02:00
|
|
|
"sync"
|
2019-06-22 20:34:19 +02:00
|
|
|
|
2019-10-25 00:27:02 +02:00
|
|
|
"github.com/kataras/iris/v12/context"
|
2019-06-22 20:34:19 +02:00
|
|
|
|
2021-02-07 01:08:34 +01:00
|
|
|
"github.com/CloudyKit/jet/v6"
|
2019-06-22 20:34:19 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const jetEngineName = "jet"
|
|
|
|
|
|
|
|
// JetEngine is the jet template parser's view engine.
|
|
|
|
type JetEngine struct {
|
2022-09-25 19:40:56 +02:00
|
|
|
fs fs.FS
|
2020-10-01 15:06:16 +02:00
|
|
|
rootDir string
|
|
|
|
extension string
|
|
|
|
left, right string
|
2020-09-05 07:34:09 +02:00
|
|
|
|
2019-06-22 20:34:19 +02:00
|
|
|
loader jet.Loader
|
|
|
|
|
|
|
|
developmentMode bool
|
|
|
|
|
|
|
|
// The Set is the `*jet.Set`, exported to offer any custom capabilities that jet users may want.
|
|
|
|
// Available after `Load`.
|
|
|
|
Set *jet.Set
|
2020-09-08 06:55:33 +02:00
|
|
|
mu sync.Mutex
|
2019-06-22 20:34:19 +02:00
|
|
|
|
|
|
|
// Note that global vars and functions are set in a single spot on the jet parser.
|
|
|
|
// If AddFunc or AddVar called before `Load` then these will be set here to be used via `Load` and clear.
|
|
|
|
vars map[string]interface{}
|
2019-08-09 07:24:58 +02:00
|
|
|
|
2020-07-06 20:40:40 +02:00
|
|
|
jetDataContextKey string
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
2020-08-06 02:35:58 +02:00
|
|
|
var (
|
|
|
|
_ Engine = (*JetEngine)(nil)
|
|
|
|
_ EngineFuncer = (*JetEngine)(nil)
|
|
|
|
)
|
2019-06-22 20:34:19 +02:00
|
|
|
|
|
|
|
// jet library does not export or give us any option to modify them via Set
|
|
|
|
// (unless we parse the files by ourselves but this is not a smart choice).
|
|
|
|
var jetExtensions = [...]string{
|
|
|
|
".html.jet",
|
|
|
|
".jet.html",
|
|
|
|
".jet",
|
|
|
|
}
|
|
|
|
|
|
|
|
// Jet creates and returns a new jet view engine.
|
2020-08-05 05:46:45 +02:00
|
|
|
// The given "extension" MUST begin with a dot.
|
2020-09-05 07:34:09 +02:00
|
|
|
//
|
|
|
|
// Usage:
|
|
|
|
// Jet("./views", ".jet") or
|
|
|
|
// Jet(iris.Dir("./views"), ".jet") or
|
2022-09-25 19:40:56 +02:00
|
|
|
// Jet(embed.FS, ".jet") or Jet(AssetFile(), ".jet") for embedded data.
|
|
|
|
func Jet(dirOrFS interface{}, extension string) *JetEngine {
|
2019-06-22 20:34:19 +02:00
|
|
|
extOK := false
|
|
|
|
for _, ext := range jetExtensions {
|
|
|
|
if ext == extension {
|
|
|
|
extOK = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if !extOK {
|
|
|
|
panic(fmt.Sprintf("%s extension is not a valid jet engine extension[%s]", extension, strings.Join(jetExtensions[0:], ", ")))
|
|
|
|
}
|
|
|
|
|
|
|
|
s := &JetEngine{
|
2022-09-25 19:40:56 +02:00
|
|
|
fs: getFS(dirOrFS),
|
2020-09-05 07:34:09 +02:00
|
|
|
rootDir: "/",
|
2020-07-06 20:40:40 +02:00
|
|
|
extension: extension,
|
2022-09-25 19:40:56 +02:00
|
|
|
loader: &jetLoader{fs: getFS(dirOrFS)},
|
2020-07-06 20:40:40 +02:00
|
|
|
jetDataContextKey: "_jet",
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// String returns the name of this view engine, the "jet".
|
|
|
|
func (s *JetEngine) String() string {
|
|
|
|
return jetEngineName
|
|
|
|
}
|
|
|
|
|
2020-09-05 07:34:09 +02:00
|
|
|
// RootDir sets the directory to be used as a starting point
|
|
|
|
// to load templates from the provided file system.
|
|
|
|
func (s *JetEngine) RootDir(root string) *JetEngine {
|
2022-09-25 19:40:56 +02:00
|
|
|
if s.fs != nil && root != "" && root != "/" && root != "." && root != s.rootDir {
|
|
|
|
sub, err := fs.Sub(s.fs, s.rootDir)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
s.fs = sub
|
|
|
|
}
|
|
|
|
|
2020-09-05 07:34:09 +02:00
|
|
|
s.rootDir = filepath.ToSlash(root)
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
// Name returns the jet engine's name.
|
|
|
|
func (s *JetEngine) Name() string {
|
|
|
|
return "Jet"
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:34:19 +02:00
|
|
|
// Ext should return the final file extension which this view engine is responsible to render.
|
2020-09-29 18:19:19 +02:00
|
|
|
// If the filename extension on ExecuteWriter is empty then this is appended.
|
2019-06-22 20:34:19 +02:00
|
|
|
func (s *JetEngine) Ext() string {
|
|
|
|
return s.extension
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delims sets the action delimiters to the specified strings, to be used in
|
|
|
|
// templates. An empty delimiter stands for the
|
|
|
|
// corresponding default: {{ or }}.
|
|
|
|
// Should act before `Load` or `iris.Application#RegisterView`.
|
|
|
|
func (s *JetEngine) Delims(left, right string) *JetEngine {
|
2020-10-01 15:06:16 +02:00
|
|
|
s.left = left
|
|
|
|
s.right = right
|
2019-06-22 20:34:19 +02:00
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// JetArguments is a type alias of `jet.Arguments`,
|
|
|
|
// can be used on `AddFunc$funcBody`.
|
|
|
|
type JetArguments = jet.Arguments
|
|
|
|
|
|
|
|
// AddFunc should adds a global function to the jet template set.
|
|
|
|
func (s *JetEngine) AddFunc(funcName string, funcBody interface{}) {
|
|
|
|
// if something like "urlpath" is registered.
|
|
|
|
if generalFunc, ok := funcBody.(func(string, ...interface{}) string); ok {
|
|
|
|
// jet, unlike others does not accept a func(string, ...interface{}) string,
|
|
|
|
// instead it wants:
|
|
|
|
// func(JetArguments) reflect.Value.
|
|
|
|
|
2020-02-05 11:36:21 +01:00
|
|
|
s.AddVar(funcName, jet.Func(func(args JetArguments) reflect.Value {
|
2019-06-22 20:34:19 +02:00
|
|
|
n := args.NumOfArguments()
|
|
|
|
if n == 0 { // no input, don't execute the function, panic instead.
|
|
|
|
panic(funcName + " expects one or more input arguments")
|
|
|
|
}
|
|
|
|
|
|
|
|
firstInput := args.Get(0).String()
|
|
|
|
|
|
|
|
if n == 1 { // if only the first argument is given.
|
|
|
|
return reflect.ValueOf(generalFunc(firstInput))
|
|
|
|
}
|
|
|
|
|
|
|
|
// if has variadic.
|
|
|
|
|
|
|
|
variadicN := n - 1
|
|
|
|
variadicInputs := make([]interface{}, variadicN) // except the first one.
|
|
|
|
|
|
|
|
for i := 0; i < variadicN; i++ {
|
|
|
|
variadicInputs[i] = args.Get(i + 1).Interface()
|
|
|
|
}
|
|
|
|
|
|
|
|
return reflect.ValueOf(generalFunc(firstInput, variadicInputs...))
|
2020-02-05 11:36:21 +01:00
|
|
|
}))
|
2019-06-22 20:34:19 +02:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if jetFunc, ok := funcBody.(jet.Func); !ok {
|
|
|
|
alternativeJetFunc, ok := funcBody.(func(JetArguments) reflect.Value)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("JetEngine.AddFunc: funcBody argument is not a type of func(JetArguments) reflect.Value. Got %T instead", funcBody))
|
|
|
|
}
|
|
|
|
|
|
|
|
s.AddVar(funcName, jet.Func(alternativeJetFunc))
|
|
|
|
} else {
|
|
|
|
s.AddVar(funcName, jetFunc)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddVar adds a global variable to the jet template set.
|
|
|
|
func (s *JetEngine) AddVar(key string, value interface{}) {
|
|
|
|
if s.Set != nil {
|
|
|
|
s.Set.AddGlobal(key, value)
|
|
|
|
} else {
|
|
|
|
if s.vars == nil {
|
|
|
|
s.vars = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
s.vars[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reload if setted to true the templates are reloading on each render,
|
|
|
|
// use it when you're in development and you're boring of restarting
|
|
|
|
// the whole app when you edit a template file.
|
|
|
|
//
|
|
|
|
// Note that if `true` is passed then only one `View -> ExecuteWriter` will be render each time,
|
|
|
|
// not safe concurrent access across clients, use it only on development state.
|
|
|
|
func (s *JetEngine) Reload(developmentMode bool) *JetEngine {
|
|
|
|
s.developmentMode = developmentMode
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetLoader can be used when the caller wants to use something like
|
2020-09-05 07:34:09 +02:00
|
|
|
// multi.Loader or httpfs.Loader.
|
2019-06-22 20:34:19 +02:00
|
|
|
func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine {
|
|
|
|
s.loader = loader
|
|
|
|
return s
|
|
|
|
}
|
|
|
|
|
2020-09-05 07:34:09 +02:00
|
|
|
type jetLoader struct {
|
2022-09-25 19:40:56 +02:00
|
|
|
fs fs.FS
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
2020-09-05 07:34:09 +02:00
|
|
|
var _ jet.Loader = (*jetLoader)(nil)
|
2019-06-22 20:34:19 +02:00
|
|
|
|
2020-09-05 07:34:09 +02:00
|
|
|
// Open opens a file from file system.
|
|
|
|
func (l *jetLoader) Open(name string) (io.ReadCloser, error) {
|
2022-09-27 21:42:42 +02:00
|
|
|
name = strings.TrimPrefix(name, "/")
|
2020-09-05 07:34:09 +02:00
|
|
|
return l.fs.Open(name)
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
2021-02-07 01:08:34 +01:00
|
|
|
// Exists checks if the template name exists by walking the list of template paths.
|
|
|
|
func (l *jetLoader) Exists(name string) bool {
|
2022-09-27 21:42:42 +02:00
|
|
|
name = strings.TrimPrefix(name, "/")
|
2021-02-07 01:08:34 +01:00
|
|
|
_, err := l.fs.Open(name)
|
|
|
|
return err == nil
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
|
|
|
|
func (s *JetEngine) Load() error {
|
2022-09-25 23:34:18 +02:00
|
|
|
rootDirName := getRootDirName(s.fs)
|
|
|
|
|
2022-09-25 19:40:56 +02:00
|
|
|
return walk(s.fs, "", func(path string, info os.FileInfo, err error) error {
|
2021-01-09 04:41:20 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-10-01 15:06:16 +02:00
|
|
|
if info == nil || info.IsDir() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.extension != "" {
|
|
|
|
if !strings.HasSuffix(path, s.extension) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-25 23:34:18 +02:00
|
|
|
if s.rootDir == rootDirName {
|
|
|
|
path = strings.TrimPrefix(path, rootDirName)
|
|
|
|
path = strings.TrimPrefix(path, "/")
|
|
|
|
}
|
|
|
|
|
2020-10-01 15:06:16 +02:00
|
|
|
buf, err := asset(s.fs, path)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
|
|
}
|
2019-06-22 20:34:19 +02:00
|
|
|
|
2020-10-01 15:06:16 +02:00
|
|
|
return s.ParseTemplate(path, string(buf))
|
|
|
|
})
|
2019-06-22 20:34:19 +02:00
|
|
|
}
|
|
|
|
|
2020-09-08 06:55:33 +02:00
|
|
|
// ParseTemplate accepts a name and contnets to parse and cache a template.
|
|
|
|
// This parser does not support funcs per template. Use the `AddFunc` instead.
|
|
|
|
func (s *JetEngine) ParseTemplate(name string, contents string) error {
|
|
|
|
s.initSet()
|
|
|
|
|
2021-02-07 01:08:34 +01:00
|
|
|
_, err := s.Set.Parse(name, contents)
|
2020-09-08 06:55:33 +02:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *JetEngine) initSet() {
|
|
|
|
s.mu.Lock()
|
|
|
|
if s.Set == nil {
|
2021-02-07 01:08:34 +01:00
|
|
|
var opts = []jet.Option{
|
|
|
|
jet.WithDelims(s.left, s.right),
|
|
|
|
}
|
2022-09-25 19:40:56 +02:00
|
|
|
if s.developmentMode && !context.IsNoOpFS(s.fs) {
|
2020-09-09 13:43:26 +02:00
|
|
|
// this check is made to avoid jet's fs lookup on noOp fs (nil passed by the developer).
|
|
|
|
// This can be produced when nil fs passed
|
|
|
|
// and only `ParseTemplate` is used.
|
2021-02-07 01:08:34 +01:00
|
|
|
opts = append(opts, jet.InDevelopmentMode())
|
2020-09-09 13:43:26 +02:00
|
|
|
}
|
2020-09-08 06:55:33 +02:00
|
|
|
|
2021-02-07 01:08:34 +01:00
|
|
|
s.Set = jet.NewSet(s.loader, opts...)
|
2020-09-08 06:55:33 +02:00
|
|
|
if s.vars != nil {
|
|
|
|
for key, value := range s.vars {
|
|
|
|
s.Set.AddGlobal(key, value)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
s.mu.Unlock()
|
|
|
|
}
|
|
|
|
|
2019-06-22 20:34:19 +02:00
|
|
|
type (
|
|
|
|
// JetRuntimeVars is a type alias for `jet.VarMap`.
|
|
|
|
// Can be used at `AddJetRuntimeVars/JetEngine.AddRuntimeVars`
|
|
|
|
// to set a runtime variable ${name} to the executing template.
|
|
|
|
JetRuntimeVars = jet.VarMap
|
|
|
|
|
|
|
|
// JetRuntime is a type alias of `jet.Runtime`,
|
|
|
|
// can be used on RuntimeVariable input function.
|
|
|
|
JetRuntime = jet.Runtime
|
|
|
|
)
|
|
|
|
|
|
|
|
// JetRuntimeVarsContextKey is the Iris Context key to keep any custom jet runtime variables.
|
|
|
|
// See `AddJetRuntimeVars` package-level function and `JetEngine.AddRuntimeVars` method.
|
|
|
|
const JetRuntimeVarsContextKey = "iris.jetvarmap"
|
|
|
|
|
|
|
|
// AddJetRuntimeVars sets or inserts runtime jet variables through the Iris Context.
|
|
|
|
// This gives the ability to add runtime variables from different handlers in the request chain,
|
|
|
|
// something that the jet template parser does not offer at all.
|
|
|
|
//
|
|
|
|
// Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}).
|
|
|
|
// See `JetEngine.AddRuntimeVars` too.
|
2020-07-10 22:21:09 +02:00
|
|
|
func AddJetRuntimeVars(ctx *context.Context, jetVarMap JetRuntimeVars) {
|
2019-06-22 20:34:19 +02:00
|
|
|
if v := ctx.Values().Get(JetRuntimeVarsContextKey); v != nil {
|
|
|
|
if vars, ok := v.(JetRuntimeVars); ok {
|
|
|
|
for key, value := range jetVarMap {
|
|
|
|
vars[key] = value
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.Values().Set(JetRuntimeVarsContextKey, jetVarMap)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddRuntimeVars sets or inserts runtime jet variables through the Iris Context.
|
|
|
|
// This gives the ability to add runtime variables from different handlers in the request chain,
|
|
|
|
// something that the jet template parser does not offer at all.
|
|
|
|
//
|
|
|
|
// Usage: view.AddJetRuntimeVars(ctx, view.JetRuntimeVars{...}).
|
|
|
|
// See `view.AddJetRuntimeVars` if package-level access is more meanful to the code flow.
|
2020-07-10 22:21:09 +02:00
|
|
|
func (s *JetEngine) AddRuntimeVars(ctx *context.Context, vars JetRuntimeVars) {
|
2019-06-22 20:34:19 +02:00
|
|
|
AddJetRuntimeVars(ctx, vars)
|
|
|
|
}
|
|
|
|
|
2019-08-09 07:24:58 +02:00
|
|
|
// ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
|
|
|
|
func (s *JetEngine) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
|
|
|
|
tmpl, err := s.Set.GetTemplate(filename)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var vars JetRuntimeVars
|
|
|
|
|
2020-07-10 22:21:09 +02:00
|
|
|
if ctx, ok := w.(*context.Context); ok {
|
2019-08-09 07:24:58 +02:00
|
|
|
runtimeVars := ctx.Values().Get(JetRuntimeVarsContextKey)
|
|
|
|
if runtimeVars != nil {
|
|
|
|
if jetVars, ok := runtimeVars.(JetRuntimeVars); ok {
|
|
|
|
vars = jetVars
|
|
|
|
}
|
|
|
|
}
|
2020-07-06 20:40:40 +02:00
|
|
|
|
2022-04-23 12:18:54 +02:00
|
|
|
if viewContextData := ctx.GetViewData(); len(viewContextData) > 0 { // fix #1876
|
|
|
|
if vars == nil {
|
|
|
|
vars = make(JetRuntimeVars)
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range viewContextData {
|
|
|
|
val, ok := v.(reflect.Value)
|
|
|
|
if !ok {
|
|
|
|
val = reflect.ValueOf(v)
|
|
|
|
}
|
|
|
|
vars[k] = val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-06 20:40:40 +02:00
|
|
|
if v := ctx.Values().Get(s.jetDataContextKey); v != nil {
|
|
|
|
if bindingData == nil {
|
|
|
|
// if bindingData is nil, try to fill them by context key (a middleware can set data).
|
|
|
|
bindingData = v
|
|
|
|
} else if m, ok := bindingData.(context.Map); ok {
|
|
|
|
// else if bindingData are passed to App/Context.View
|
|
|
|
// and it's map try to fill with the new values passed from a middleware.
|
|
|
|
if mv, ok := v.(context.Map); ok {
|
|
|
|
for key, value := range mv {
|
|
|
|
m[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-23 12:18:54 +02:00
|
|
|
|
2019-08-09 07:24:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if bindingData == nil {
|
|
|
|
return tmpl.Execute(w, vars, nil)
|
|
|
|
}
|
|
|
|
|
2020-02-05 11:36:21 +01:00
|
|
|
if vars == nil {
|
|
|
|
vars = make(JetRuntimeVars)
|
2019-08-09 07:24:58 +02:00
|
|
|
}
|
|
|
|
|
2020-07-06 20:40:40 +02:00
|
|
|
/* fixed on jet v4.0.0, so no need of this:
|
2019-08-09 07:24:58 +02:00
|
|
|
if m, ok := bindingData.(context.Map); ok {
|
2020-02-05 11:36:21 +01:00
|
|
|
var jetData interface{}
|
2019-08-09 07:24:58 +02:00
|
|
|
for k, v := range m {
|
2020-07-06 20:40:40 +02:00
|
|
|
if k == s.jetDataContextKey {
|
2020-02-05 11:36:21 +01:00
|
|
|
jetData = v
|
2019-08-09 07:24:58 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-02-05 11:36:21 +01:00
|
|
|
if value, ok := v.(reflect.Value); ok {
|
|
|
|
vars[k] = value
|
|
|
|
} else {
|
|
|
|
vars[k] = reflect.ValueOf(v)
|
2019-08-09 07:24:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-05 11:36:21 +01:00
|
|
|
if jetData != nil {
|
|
|
|
bindingData = jetData
|
|
|
|
}
|
2020-07-06 20:40:40 +02:00
|
|
|
}*/
|
2019-06-22 20:34:19 +02:00
|
|
|
|
|
|
|
return tmpl.Execute(w, vars, bindingData)
|
|
|
|
}
|