From 076d9121f157eefc6aec4ede11f899614d81e237 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 22 Jun 2019 21:34:19 +0300 Subject: [PATCH] Implement a new View Engine for the Jet template parser as requested at: https://github.com/kataras/iris/issues/1281 Former-commit-id: 3e00bdfbf1f3998a1744c390c12fd70430ac0320 --- _examples/README.md | 3 + _examples/sessions/database/redis/main.go | 2 +- _examples/view/overview/main.go | 5 - _examples/view/template_html_0/main.go | 6 - _examples/view/template_jet_0/README.md | 11 + _examples/view/template_jet_0/main.go | 131 ++++++ .../views/layouts/application.jet | 10 + .../view/template_jet_0/views/todos/index.jet | 30 ++ .../view/template_jet_0/views/todos/show.jet | 9 + .../view/template_jet_1_embedded/README.md | 25 ++ .../view/template_jet_1_embedded/bindata.go | 387 +++++++++++++++++ .../view/template_jet_1_embedded/main.go | 31 ++ .../views/includes/_partial.jet | 1 + .../views/includes/blocks.jet | 3 + .../template_jet_1_embedded/views/index.jet | 16 + .../views/layouts/application.jet | 10 + context/context.go | 2 +- doc.go | 6 +- iris.go | 13 +- view/README.md | 43 +- view/engine.go | 12 + view/jet.go | 408 ++++++++++++++++++ 22 files changed, 1124 insertions(+), 40 deletions(-) create mode 100644 _examples/view/template_jet_0/README.md create mode 100644 _examples/view/template_jet_0/main.go create mode 100644 _examples/view/template_jet_0/views/layouts/application.jet create mode 100644 _examples/view/template_jet_0/views/todos/index.jet create mode 100644 _examples/view/template_jet_0/views/todos/show.jet create mode 100644 _examples/view/template_jet_1_embedded/README.md create mode 100644 _examples/view/template_jet_1_embedded/bindata.go create mode 100644 _examples/view/template_jet_1_embedded/main.go create mode 100644 _examples/view/template_jet_1_embedded/views/includes/_partial.jet create mode 100644 _examples/view/template_jet_1_embedded/views/includes/blocks.jet create mode 100644 _examples/view/template_jet_1_embedded/views/index.jet create mode 100644 _examples/view/template_jet_1_embedded/views/layouts/application.jet create mode 100644 view/jet.go diff --git a/_examples/README.md b/_examples/README.md index a8f5098c..5237c6ed 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -339,6 +339,7 @@ Follow the examples below, | handlebars | `iris.Handlebars(...)` | | amber | `iris.Amber(...)` | | pug(jade) | `iris.Pug(...)` | +| jet | `iris.Jet(...)` | - [Overview](view/overview/main.go) - [Hi](view/template_html_0/main.go) @@ -353,6 +354,8 @@ Follow the examples below, - [Pug (Jade) Actions`](view/template_pug_1) - [Pug (Jade) Includes`](view/template_pug_2) - [Pug (Jade) Extends`](view/template_pug_3) +- [Jet](/view/template_jet_0) **NEW** +- [Jet Embedded](view/template_jet_1_embedded) **NEW** You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples. diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index d424aa5a..36d7cca2 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -32,7 +32,7 @@ func main() { sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", - Expires: 0, // defauls to 0: unlimited life. Another good value is: 45 * time.Minute, + Expires: 0, // defaults to 0: unlimited life. Another good value is: 45 * time.Minute, AllowReclaim: true, }) diff --git a/_examples/view/overview/main.go b/_examples/view/overview/main.go index 38756d93..0c3e9a27 100644 --- a/_examples/view/overview/main.go +++ b/_examples/view/overview/main.go @@ -5,11 +5,6 @@ import "github.com/kataras/iris" func main() { app := iris.New() - // - standard html | iris.HTML(...) - // - django | iris.Django(...) - // - pug(jade) | iris.Pug(...) - // - handlebars | iris.Handlebars(...) - // - amber | iris.Amber(...) // with default template funcs: // // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} diff --git a/_examples/view/template_html_0/main.go b/_examples/view/template_html_0/main.go index efc997f0..fe65469d 100644 --- a/_examples/view/template_html_0/main.go +++ b/_examples/view/template_html_0/main.go @@ -7,12 +7,6 @@ import ( func main() { app := iris.New() // defaults to these - // - standard html | iris.HTML(...) - // - django | iris.Django(...) - // - pug(jade) | iris.Pug(...) - // - handlebars | iris.Handlebars(...) - // - amber | iris.Amber(...) - tmpl := iris.HTML("./templates", ".html") tmpl.Reload(true) // reload templates on each request (development mode) // default template funcs are: diff --git a/_examples/view/template_jet_0/README.md b/_examples/view/template_jet_0/README.md new file mode 100644 index 00000000..3957c5ce --- /dev/null +++ b/_examples/view/template_jet_0/README.md @@ -0,0 +1,11 @@ +# Jet Engine Example + +This example is a fork of to work with Iris, so you can +notice the differences side by side. + +Read more at: https://github.com/kataras/iris/issues/1281 + +> The Iris Jet View Engine fixes some bugs that the underline [jet template parser](https://github.com/CloudyKit/jet) currently has. + + +Continue by learning how you can [serve embedded templates](../template_jet_1_embedded). \ No newline at end of file diff --git a/_examples/view/template_jet_0/main.go b/_examples/view/template_jet_0/main.go new file mode 100644 index 00000000..b227dce4 --- /dev/null +++ b/_examples/view/template_jet_0/main.go @@ -0,0 +1,131 @@ +// Package main shows how to use jet template parser with ease using the Iris built-in Jet view engine. +// This example is customized fork of https://github.com/CloudyKit/jet/tree/master/examples/todos, so you can +// notice the differences side by side. +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "os" + "reflect" + "strings" + + "github.com/kataras/iris" + "github.com/kataras/iris/view" +) + +type tTODO struct { + Text string + Done bool +} + +type doneTODOs struct { + list map[string]*tTODO + keys []string + len int + i int +} + +func (dt *doneTODOs) New(todos map[string]*tTODO) *doneTODOs { + dt.len = len(todos) + for k := range todos { + dt.keys = append(dt.keys, k) + } + dt.list = todos + return dt +} + +// Range satisfies the jet.Ranger interface and only returns TODOs that are done, +// even when the list contains TODOs that are not done. +func (dt *doneTODOs) Range() (reflect.Value, reflect.Value, bool) { + for dt.i < dt.len { + key := dt.keys[dt.i] + dt.i++ + if dt.list[key].Done { + return reflect.ValueOf(key), reflect.ValueOf(dt.list[key]), false + } + } + return reflect.Value{}, reflect.Value{}, true +} + +func (dt *doneTODOs) Render(r *view.JetRuntime) { + r.Write([]byte(fmt.Sprintf("custom renderer"))) +} + +// Render implements jet.Renderer interface +func (t *tTODO) Render(r *view.JetRuntime) { + done := "yes" + if !t.Done { + done = "no" + } + r.Write([]byte(fmt.Sprintf("TODO: %s (done: %s)", t.Text, done))) +} + +func main() { + // + // Type aliases: + // view.JetRuntimeVars = jet.VarMap + // view.JetRuntime = jet.Runtime + // view.JetArguments = jet.Arguments + // + // Iris also gives you the ability to put runtime variables + // from middlewares as well, by: + // view.AddJetRuntimeVars(ctx, vars) + // or tmpl.AddRuntimeVars(ctx, vars) + // + // The Iris Jet fixes the issue when custom jet.Ranger and custom jet.Renderer are not actually work at all, + // hope that this is a temp issue on the jet parser itself and authors will fix it soon + // so we can remove the "hacks" we've putted for those to work as expected. + + app := iris.New() + tmpl := iris.Jet("./views", ".jet") // <-- + tmpl.Reload(true) // remove in production. + tmpl.AddFunc("base64", func(a view.JetArguments) reflect.Value { + a.RequireNumOfArguments("base64", 1, 1) + + buffer := bytes.NewBuffer(nil) + fmt.Fprint(buffer, a.Get(0)) + + return reflect.ValueOf(base64.URLEncoding.EncodeToString(buffer.Bytes())) + }) + app.RegisterView(tmpl) // <-- + + var todos = map[string]*tTODO{ + "example-todo-1": {Text: "Add an show todo page to the example project", Done: true}, + "example-todo-2": {Text: "Add an add todo page to the example project"}, + "example-todo-3": {Text: "Add an update todo page to the example project"}, + "example-todo-4": {Text: "Add an delete todo page to the example project", Done: true}, + } + + app.Get("/", func(ctx iris.Context) { + ctx.View("todos/index.jet", todos) // <-- + // Note that the `ctx.View` already logs the error if logger level is allowing it and returns the error. + }) + + app.Get("/todo", func(ctx iris.Context) { + id := ctx.URLParam("id") + todo, ok := todos[id] + if !ok { + ctx.Redirect("/") + return + } + + ctx.View("todos/show.jet", todo) + }) + app.Get("/all-done", func(ctx iris.Context) { + vars := make(view.JetRuntimeVars) // <-- or keep use the jet.VarMap, decision up to you, it refers to the same type. + vars.Set("showingAllDone", true) + view.AddJetRuntimeVars(ctx, vars) // <-- + ctx.View("todos/index.jet", (&doneTODOs{}).New(todos)) + }) + + port := os.Getenv("PORT") + if len(port) == 0 { + port = ":8080" + } else if !strings.HasPrefix(":", port) { + port = ":" + port + } + + app.Run(iris.Addr(port)) +} diff --git a/_examples/view/template_jet_0/views/layouts/application.jet b/_examples/view/template_jet_0/views/layouts/application.jet new file mode 100644 index 00000000..9fce2365 --- /dev/null +++ b/_examples/view/template_jet_0/views/layouts/application.jet @@ -0,0 +1,10 @@ + + + + + {{ isset(title) ? title : "" }} + + + {{block documentBody()}}{{end}} + + diff --git a/_examples/view/template_jet_0/views/todos/index.jet b/_examples/view/template_jet_0/views/todos/index.jet new file mode 100644 index 00000000..884f1115 --- /dev/null +++ b/_examples/view/template_jet_0/views/todos/index.jet @@ -0,0 +1,30 @@ +{{extends "layouts/application.jet"}} + +{{block button(label, href="javascript:void(0)")}} + {{ label }} +{{end}} + +{{block ul()}} + +{{end}} + +{{block documentBody()}} +

List of TODOs

+ + {{if isset(showingAllDone) && showingAllDone}} +

Showing only TODOs that are done

+ {{else}} +

Show only TODOs that are done

+ {{end}} + + {{yield ul() content}} + {{range id, value := .}} +
  • + {{ value.Text }} + {{yield button(label="UP", href="/update/?id="+base64(id))}} - {{yield button(href="/delete/?id="+id, label="DL")}} +
  • + {{end}} + {{end}} +{{end}} diff --git a/_examples/view/template_jet_0/views/todos/show.jet b/_examples/view/template_jet_0/views/todos/show.jet new file mode 100644 index 00000000..334e7813 --- /dev/null +++ b/_examples/view/template_jet_0/views/todos/show.jet @@ -0,0 +1,9 @@ +{{extends "layouts/application.jet"}} + +{{block documentBody()}} +

    Show TODO

    +

    This uses a custom renderer by implementing the jet.Renderer interface. +

    + {{.}} +

    +{{end}} diff --git a/_examples/view/template_jet_1_embedded/README.md b/_examples/view/template_jet_1_embedded/README.md new file mode 100644 index 00000000..a6315743 --- /dev/null +++ b/_examples/view/template_jet_1_embedded/README.md @@ -0,0 +1,25 @@ +# Jet Engine Example (Embedded) + +Take a look at the [../template_jet_0](../template_jet_0)'s README first. + +This example teaches you how to use jet templates embedded in your applications with ease using the Iris built-in Jet view engine. + +This example is a customized fork of https://github.com/CloudyKit/jet/tree/master/examples/asset_packaging, so you can +notice the differences side by side. For example, you don't have to use any external package inside your application, +Iris manually builds the template loader for binary data when Asset and AssetNames are available through tools like the [go-bindata](github.com/shuLhan/go-bindata). + +Note that you can still use any custom loaders through the `JetEngine.SetLoader` +which overrides any previous loaders like `JetEngine.Binary` we use on this example. + +## How to run + +```sh +$ go get -u github.com/shuLhan/go-bindata/... # or any active alternative +$ go-bindata ./views/... +$ go build +$ ./template_jet_0_embedded +``` + +Repeat the above steps on any `./views` changes. + +> html files are not used, only binary data. You can move or delete the `./views` folder. diff --git a/_examples/view/template_jet_1_embedded/bindata.go b/_examples/view/template_jet_1_embedded/bindata.go new file mode 100644 index 00000000..2d9e1519 --- /dev/null +++ b/_examples/view/template_jet_1_embedded/bindata.go @@ -0,0 +1,387 @@ +// Code generated by go-bindata. DO NOT EDIT. +// sources: +// views\includes\_partial.jet +// views\includes\blocks.jet +// views\index.jet +// views\layouts\application.jet + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info fileInfoEx +} + +type fileInfoEx interface { + os.FileInfo + MD5Checksum() string +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time + md5checksum string +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) MD5Checksum() string { + return fi.md5checksum +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _bindataViewsincludespartialjet = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb2\x29\xb0\xf3\xcc\x4b\xce\x29\x4d\x49\x4d\x51\x28\x48\x2c\x2a\xc9\x4c" + + "\xcc\xb1\xd1\x2f\xb0\xe3\x02\x04\x00\x00\xff\xff\xd0\x10\x20\x0a\x18\x00\x00\x00") + +func bindataViewsincludespartialjetBytes() ([]byte, error) { + return bindataRead( + _bindataViewsincludespartialjet, + "views/includes/_partial.jet", + ) +} + +func bindataViewsincludespartialjet() (*asset, error) { + bytes, err := bindataViewsincludespartialjetBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "views/includes/_partial.jet", + size: 24, + md5checksum: "", + mode: os.FileMode(438), + modTime: time.Unix(1533831061, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + +var _bindataViewsincludesblocksjet = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\x4e\xca\xc9\x4f\xce\x56\xc8\x4d\xcd\x2b\xd5\xd0\xac\xad\xe5\x52" + + "\x50\xb0\x29\xb0\x0b\xc9\x48\x05\x8b\x28\x40\x24\xcb\x13\x8b\x15\x32\xf3\xca\xf2\xb3\x53\x53\xf4\x6c\xf4\x0b\xec" + + "\xb8\xaa\xab\x53\xf3\x52\x6a\x6b\xb9\x00\x01\x00\x00\xff\xff\x3f\xde\x27\x27\x3e\x00\x00\x00") + +func bindataViewsincludesblocksjetBytes() ([]byte, error) { + return bindataRead( + _bindataViewsincludesblocksjet, + "views/includes/blocks.jet", + ) +} + +func bindataViewsincludesblocksjet() (*asset, error) { + bytes, err := bindataViewsincludesblocksjetBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "views/includes/blocks.jet", + size: 62, + md5checksum: "", + mode: os.FileMode(438), + modTime: time.Unix(1533831061, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + +var _bindataViewsindexjet = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x90\xb1\x6e\xf3\x30\x0c\x84\x77\x3d\xc5\xfd\x5e\xfe\x74\xa9\x91\xb5" + + "\x30\x3c\x14\xc8\xd0\xa5\x4b\x1f\xa0\x50\x2c\x06\x56\x23\x8b\x42\x48\xd7\x36\x04\xbf\x7b\x11\xc5\x29\x8a\x76\x22" + + "\xc8\xfb\x8e\x3c\x30\x67\x9a\x95\xa2\x13\x54\xc1\x2e\x3c\xaa\xd4\x36\xa5\xe0\x3b\xab\x9e\xe3\xe3\x07\x69\xb5\xae" + + "\x26\x67\x3f\x24\xbe\x28\x2a\x1f\xbb\x30\x3a\x92\xfa\x18\xb8\x3b\xcb\x1d\x30\x39\x97\x01\x1c\x77\xe3\x40\x51\x9f" + + "\xd9\x2d\xbb\x87\x75\x35\x40\xd3\xef\xdb\xc3\x70\x24\xe7\xc8\x81\x66\x3b\xa4\x40\x4d\xdd\xef\x5b\x73\x15\xa3\xfd" + + "\x6c\x0d\x00\xe4\xbc\x78\x0a\x0e\x03\xc5\xf1\xee\xac\x8b\x6a\xae\xe2\x76\xf8\x47\x82\xf7\x64\x2f\xea\x6d\xf8\xce" + + "\x50\xb0\x13\xfe\x6d\xc4\xcb\xe9\x30\x7b\x51\xd9\x55\x8e\x49\x5e\x59\x4b\x5b\xe8\xdb\x7a\xa0\x49\xed\x5b\xcf\x93" + + "\xa0\xe7\xe9\x8f\x0f\x13\x5f\xce\xf2\x84\xdf\x6e\x4c\x56\x10\x59\xb1\xf1\x0e\x47\xea\xec\x28\x04\xaf\x05\x8e\xff" + + "\x15\x54\xe8\xa6\x4e\x6d\x89\x45\xd1\x95\x37\xde\xea\x57\x00\x00\x00\xff\xff\xff\xe0\x1f\xae\x75\x01\x00\x00") + +func bindataViewsindexjetBytes() ([]byte, error) { + return bindataRead( + _bindataViewsindexjet, + "views/index.jet", + ) +} + +func bindataViewsindexjet() (*asset, error) { + bytes, err := bindataViewsindexjetBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "views/index.jet", + size: 373, + md5checksum: "", + mode: os.FileMode(438), + modTime: time.Unix(1561227802, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + +var _bindataViewslayoutsapplicationjet = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\x8e\xbd\xae\xc2\x30\x0c\x85\xf7\x3e\x85\x6f\xa6\xcb\x80\xba\x32\xb8" + + "\x1d\x80\xb2\xc2\x50\x06\xc6\x34\xb6\x08\x22\x3f\x08\x8c\x44\x15\xe5\xdd\x51\x08\x4c\xb6\xec\xf3\x7d\x3a\xf8\xb7" + + "\xdd\x6f\xc6\xd3\x61\x00\x2b\xde\xf5\x0d\x96\x01\x4e\x87\x73\xa7\x38\xa8\xbe\x01\x40\xcb\x9a\xca\x02\x80\x9e\x45" + + "\x83\xb1\xfa\xfe\x60\xe9\xd4\x71\xdc\x2d\x57\xea\xfb\x92\x8b\x38\xee\x07\x3f\x31\x11\x13\xf0\x4b\xfb\x9b\x63\x6c" + + "\xeb\xbd\x78\xda\x9f\x08\xa7\x48\x73\xc5\x52\x9a\x5c\x34\x57\xa0\x68\x9e\x9e\x83\xac\x23\xcd\xff\x8b\x9c\x53\xe2" + + "\x40\x39\x7f\xb0\x9a\xc6\xb6\x36\x7c\x07\x00\x00\xff\xff\x76\x86\x91\x20\xb2\x00\x00\x00") + +func bindataViewslayoutsapplicationjetBytes() ([]byte, error) { + return bindataRead( + _bindataViewslayoutsapplicationjet, + "views/layouts/application.jet", + ) +} + +func bindataViewslayoutsapplicationjet() (*asset, error) { + bytes, err := bindataViewslayoutsapplicationjetBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "views/layouts/application.jet", + size: 178, + md5checksum: "", + mode: os.FileMode(438), + modTime: time.Unix(1561227776, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + +// +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +// +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} +} + +// +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +// nolint: deadcode +// +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or could not be loaded. +// +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} +} + +// +// AssetNames returns the names of the assets. +// nolint: deadcode +// +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// +// _bindata is a table, holding each asset generator, mapped to its name. +// +var _bindata = map[string]func() (*asset, error){ + "views/includes/_partial.jet": bindataViewsincludespartialjet, + "views/includes/blocks.jet": bindataViewsincludesblocksjet, + "views/index.jet": bindataViewsindexjet, + "views/layouts/application.jet": bindataViewslayoutsapplicationjet, +} + +// +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +// +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } + } + } + } + if node.Func != nil { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ + "views": {Func: nil, Children: map[string]*bintree{ + "includes": {Func: nil, Children: map[string]*bintree{ + "_partial.jet": {Func: bindataViewsincludespartialjet, Children: map[string]*bintree{}}, + "blocks.jet": {Func: bindataViewsincludesblocksjet, Children: map[string]*bintree{}}, + }}, + "index.jet": {Func: bindataViewsindexjet, Children: map[string]*bintree{}}, + "layouts": {Func: nil, Children: map[string]*bintree{ + "application.jet": {Func: bindataViewslayoutsapplicationjet, Children: map[string]*bintree{}}, + }}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/_examples/view/template_jet_1_embedded/main.go b/_examples/view/template_jet_1_embedded/main.go new file mode 100644 index 00000000..8ceb253a --- /dev/null +++ b/_examples/view/template_jet_1_embedded/main.go @@ -0,0 +1,31 @@ +// Package main shows how to use jet templates embedded in your application with ease using the Iris built-in Jet view engine. +// This example is a customized fork of https://github.com/CloudyKit/jet/tree/master/examples/asset_packaging, so you can +// notice the differences side by side. For example, you don't have to use any external package inside your application, +// Iris manually builds the template loader for binary data when Asset and AssetNames are available via tools like the go-bindata. +package main + +import ( + "os" + "strings" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + tmpl := iris.Jet("./views", ".jet").Binary(Asset, AssetNames) + app.RegisterView(tmpl) + + app.Get("/", func(ctx iris.Context) { + ctx.View("index.jet") + }) + + port := os.Getenv("PORT") + if len(port) == 0 { + port = ":8080" + } else if !strings.HasPrefix(":", port) { + port = ":" + port + } + + app.Run(iris.Addr(port)) +} diff --git a/_examples/view/template_jet_1_embedded/views/includes/_partial.jet b/_examples/view/template_jet_1_embedded/views/includes/_partial.jet new file mode 100644 index 00000000..3ebaa7af --- /dev/null +++ b/_examples/view/template_jet_1_embedded/views/includes/_partial.jet @@ -0,0 +1 @@ +

    Included partial

    diff --git a/_examples/view/template_jet_1_embedded/views/includes/blocks.jet b/_examples/view/template_jet_1_embedded/views/includes/blocks.jet new file mode 100644 index 00000000..14c074ea --- /dev/null +++ b/_examples/view/template_jet_1_embedded/views/includes/blocks.jet @@ -0,0 +1,3 @@ +{{block menu()}} +

    The menu block was invoked.

    +{{end}} diff --git a/_examples/view/template_jet_1_embedded/views/index.jet b/_examples/view/template_jet_1_embedded/views/index.jet new file mode 100644 index 00000000..670f11a7 --- /dev/null +++ b/_examples/view/template_jet_1_embedded/views/index.jet @@ -0,0 +1,16 @@ +{{extends "layouts/application.jet"}} +{{import "includes/blocks.jet"}} + +{{block documentBody()}} +

    Embedded example

    + + + + {{include "includes/_partial.jet"}} + + {{if !includeIfExists("doesNotExist.jet")}} +

    Shows how !includeIfExists works: doesNotExist.jet was not included because it doesn't exist.

    + {{end}} +{{end}} diff --git a/_examples/view/template_jet_1_embedded/views/layouts/application.jet b/_examples/view/template_jet_1_embedded/views/layouts/application.jet new file mode 100644 index 00000000..16117b71 --- /dev/null +++ b/_examples/view/template_jet_1_embedded/views/layouts/application.jet @@ -0,0 +1,10 @@ + + + + + Embedded example + + + {{block documentBody()}}{{end}} + + diff --git a/context/context.go b/context/context.go index 56f96302..61944db2 100644 --- a/context/context.go +++ b/context/context.go @@ -2790,7 +2790,7 @@ func (ctx *context) View(filename string, optionalViewModel ...interface{}) erro bindingData = ctx.values.Get(cfg.GetViewDataContextKey()) } - err := ctx.Application().View(ctx.writer, filename, layout, bindingData) + err := ctx.Application().View(ctx, filename, layout, bindingData) if err != nil { ctx.StatusCode(http.StatusInternalServerError) ctx.StopExecution() diff --git a/doc.go b/doc.go index cdc73cfe..2ecf9a7d 100644 --- a/doc.go +++ b/doc.go @@ -1069,7 +1069,7 @@ Example code: View Engine -Iris supports 5 template engines out-of-the-box, developers can still use any external golang template engine, +Iris supports 6 template engines out-of-the-box, developers can still use any external golang template engine, as `context/context#ResponseWriter()` is an `io.Writer`. All of these five template engines have common features with common API, @@ -1090,6 +1090,8 @@ like Layout, Template Funcs, Party-specific layout, partial rendering and more. Amber, its template parser is the github.com/eknkc/amber + Jet, + its template parser is the github.com/CloudyKit/jet Example code: @@ -1105,6 +1107,7 @@ Example code: // - pug(jade) | iris.Pug(...) // - handlebars | iris.Handlebars(...) // - amber | iris.Amber(...) + // - jet | iris.Jet(...) tmpl := iris.HTML("./templates", ".html") tmpl.Reload(true) // reload templates on each request (development mode) @@ -1195,6 +1198,7 @@ access to the engines' variables can be granded by "github.com/kataras/iris" pac iris.Pug(...) >> >> view.Pug(...) iris.Handlebars(...) >> >> view.Handlebars(...) iris.Amber(...) >> >> view.Amber(...) + iris.Jet(...) >> >> view.Jet(...) Each one of these template engines has different options located here: https://github.com/kataras/iris/tree/master/view . diff --git a/iris.go b/iris.go index 1e1bfa87..4cae8059 100644 --- a/iris.go +++ b/iris.go @@ -286,20 +286,23 @@ func (app *Application) Logger() *golog.Logger { var ( // HTML view engine. - // Conversion for the view.HTML. + // Shortcut of the kataras/iris/view.HTML. HTML = view.HTML // Django view engine. - // Conversion for the view.Django. + // Shortcut of the kataras/iris/view.Django. Django = view.Django // Handlebars view engine. - // Conversion for the view.Handlebars. + // Shortcut of the kataras/iris/view.Handlebars. Handlebars = view.Handlebars // Pug view engine. - // Conversion for the view.Pug. + // Shortcut of the kataras/iris/view.Pug. Pug = view.Pug // Amber view engine. - // Conversion for the view.Amber. + // Shortcut of the kataras/iris/view.Amber. Amber = view.Amber + // Jet view engine. + // Shortcut of the kataras/iris/view.Jet. + Jet = view.Jet ) // NoLayout to disable layout for a particular template file diff --git a/view/README.md b/view/README.md index 48a900b3..fedcb9e8 100644 --- a/view/README.md +++ b/view/README.md @@ -1,6 +1,6 @@ # View -Iris supports 5 template engines out-of-the-box, developers can still use any external golang template engine, +Iris supports 6 template engines out-of-the-box, developers can still use any external golang template engine, as `context/context#ResponseWriter()` is an `io.Writer`. All of these five template engines have common features with common API, @@ -11,6 +11,26 @@ like Layout, Template Funcs, Party-specific layout, partial rendering and more. - Pug(Jade), its template parser is the [github.com/Joker/jade](https://github.com/Joker/jade) - Handlebars, its template parser is the [github.com/aymerick/raymond](https://github.com/aymerick/raymond) - Amber, its template parser is the [github.com/eknkc/amber](https://github.com/eknkc/amber) +- Jet, its template parser is the [github.com/CloudyKit/jet](https://github.com/CloudyKit/jet) + +## Examples + +- [Overview](https://github.com/kataras/iris/blob/master/_examples/view/overview/main.go) +- [Hi](https://github.com/kataras/iris/blob/master/_examples/view/template_html_0/main.go) +- [A simple Layout](https://github.com/kataras/iris/blob/master/_examples/view/template_html_1/main.go) +- [Layouts: `yield` and `render` tmpl funcs](https://github.com/kataras/iris/blob/master/_examples/view/template_html_2/main.go) +- [The `urlpath` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_3/main.go) +- [The `url` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_4/main.go) +- [Inject Data Between Handlers](https://github.com/kataras/iris/blob/master/_examples/view/context-view-data/main.go) +- [Embedding Templates Into App Executable File](https://github.com/kataras/iris/blob/master/_examples/view/embedding-templates-into-app/main.go) +- [Greeting with Pug (Jade)`](view/template_pug_0) +- [Pug (Jade) Actions`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_1) +- [Pug (Jade) Includes`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_2) +- [Pug (Jade) Extends`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_3) +- [Jet](https://github.com/kataras/iris/blob/master/_examples/view/template_jet_0) **NEW** +- [Jet Embedded](https://github.com/kataras/iris/blob/master/_examples/view/template_jet_1_embedded) **NEW** + +You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `context#ResponseWriter`, take a look at the [iris/_examples/http_responsewriter/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/http_responsewriter/quicktemplate) example. ## Overview @@ -69,12 +89,6 @@ import "github.com/kataras/iris" func main() { app := iris.New() - - // - standard html | iris.HTML(...) - // - django | iris.Django(...) - // - pug(jade) | iris.Pug(...) - // - handlebars | iris.Handlebars(...) - // - amber | iris.Amber(...) tmpl := iris.HTML("./templates", ".html") // builtin template funcs are: @@ -161,17 +175,4 @@ Example code: pugEngine := iris.Pug("./templates", ".jade") pugEngine.Reload(true) // <--- set to true to re-build the templates on each request. app.RegisterView(pugEngine) -``` - -## Examples - -- [Overview](https://github.com/kataras/iris/blob/master/_examples/view/overview/main.go) -- [Hi](https://github.com/kataras/iris/blob/master/_examples/view/template_html_0/main.go) -- [A simple Layout](https://github.com/kataras/iris/blob/master/_examples/view/template_html_1/main.go) -- [Layouts: `yield` and `render` tmpl funcs](https://github.com/kataras/iris/blob/master/_examples/view/template_html_2/main.go) -- [The `urlpath` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_3/main.go) -- [The `url` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_4/main.go) -- [Inject Data Between Handlers](https://github.com/kataras/iris/blob/master/_examples/view/context-view-data/main.go) -- [Embedding Templates Into App Executable File](https://github.com/kataras/iris/blob/master/_examples/view/embedding-templates-into-app/main.go) - -You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `context#ResponseWriter`, take a look at the [iris/_examples/http_responsewriter/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/http_responsewriter/quicktemplate) example. \ No newline at end of file +``` \ No newline at end of file diff --git a/view/engine.go b/view/engine.go index 70fac87f..4d5dd318 100644 --- a/view/engine.go +++ b/view/engine.go @@ -29,3 +29,15 @@ type Engine interface { // Ext should return the final file extension which this view engine is responsible to render. Ext() string } + +type namedEngine interface { + String() string +} + +func getEngineName(e Engine) string { + if n, ok := e.(namedEngine); ok { + return n.String() + } + + return "" +} diff --git a/view/jet.go b/view/jet.go new file mode 100644 index 00000000..727517ae --- /dev/null +++ b/view/jet.go @@ -0,0 +1,408 @@ +package view + +import ( + "fmt" + "io" + "os" + "path" + "reflect" + "strings" + + "github.com/kataras/iris/context" + + "github.com/CloudyKit/jet" +) + +const jetEngineName = "jet" + +// JetEngine is the jet template parser's view engine. +type JetEngine struct { + directory string + extension string + // physical system files or app-embedded, see `Binary(..., ...)`. Defaults to file system on initialization. + 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 + + // 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{} +} + +var _ Engine = (*JetEngine)(nil) + +// 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. +func Jet(directory, extension string) *JetEngine { + 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{ + directory: directory, + extension: extension, + loader: jet.NewOSFileSystemLoader(directory), + } + + return s +} + +// String returns the name of this view engine, the "jet". +func (s *JetEngine) String() string { + return jetEngineName +} + +// Ext should return the final file extension which this view engine is responsible to render. +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 { + s.Set.Delims(left, right) + 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. + + s.AddVar(funcName, func(args JetArguments) reflect.Value { + 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...)) + }) + + 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 + if s.Set != nil { + s.Set.SetDevelopmentMode(developmentMode) + } + return s +} + +// SetLoader can be used when the caller wants to use something like +// multi.Loader or httpfs.Loader of the jet subpackages, +// overrides any previous loader may set by `Binary` or the default. +// Should act before `Load` or `iris.Application#RegisterView`. +func (s *JetEngine) SetLoader(loader jet.Loader) *JetEngine { + s.loader = loader + return s +} + +// Binary optionally, use it when template files are distributed +// inside the app executable (.go generated files). +// +// The assetFn and namesFn can come from the go-bindata library. +// Should act before `Load` or `iris.Application#RegisterView`. +func (s *JetEngine) Binary(assetFn func(name string) ([]byte, error), assetNames func() []string) *JetEngine { + // embedded. + vdir := s.directory + + if vdir[0] == '.' { + vdir = vdir[1:] + } + + // second check for /something, (or ./something if we had dot on 0 it will be removed) + if vdir[0] == '/' || vdir[0] == os.PathSeparator { + vdir = vdir[1:] + } + + // check for trailing slashes because new users may be do that by mistake + // although all examples are showing the correct way but you never know + // i.e "./assets/" is not correct, if was inside "./assets". + // remove last "/". + if trailingSlashIdx := len(vdir) - 1; vdir[trailingSlashIdx] == '/' { + vdir = vdir[0:trailingSlashIdx] + } + + namesSlice := assetNames() + names := make(map[string]struct{}) + for _, name := range namesSlice { + if !strings.HasPrefix(name, vdir) { + continue + } + + extOK := false + fileExt := path.Ext(name) + for _, ext := range jetExtensions { + if ext == fileExt { + extOK = true + break + } + } + + if !extOK { + continue + } + + names[name] = struct{}{} + } + + if len(names) == 0 { + panic("JetEngine.Binary: no embedded files found in directory: " + vdir) + } + + s.loader = &embeddedLoader{ + vdir: vdir, + asset: assetFn, + names: names, + } + return s +} + +type ( + embeddedLoader struct { + vdir string + asset func(name string) ([]byte, error) + names map[string]struct{} + } + embeddedFile struct { + contents []byte // the contents are NOT consumed. + readen int64 + } +) + +var ( + _ jet.Loader = (*embeddedLoader)(nil) + _ io.ReadCloser = (*embeddedFile)(nil) +) + +func (f *embeddedFile) Close() error { return nil } +func (f *embeddedFile) Read(p []byte) (int, error) { + if f.readen >= int64(len(f.contents)) { + return 0, io.EOF + } + + n := copy(p, f.contents[f.readen:]) + f.readen += int64(n) + return n, nil +} + +// Open opens a file from OS file system. +func (l *embeddedLoader) Open(name string) (io.ReadCloser, error) { + // name = path.Join(l.vdir, name) + contents, err := l.asset(name) + if err != nil { + return nil, err + } + return &embeddedFile{ + contents: contents, + }, nil +} + +// Exists checks if the template name exists by walking the list of template paths +// returns string with the full path of the template and bool true if the template file was found +func (l *embeddedLoader) Exists(name string) (string, bool) { + fileName := path.Join(l.vdir, name) + if _, ok := l.names[fileName]; ok { + return fileName, true + } + + return "", false +} + +// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata). +func (s *JetEngine) Load() error { + s.Set = jet.NewHTMLSetLoader(s.loader) + s.Set.SetDevelopmentMode(s.developmentMode) + + if s.vars != nil { + for key, value := range s.vars { + s.Set.AddGlobal(key, value) + } + } + + // Note that, unlike the rest of template engines implementations, + // we don't call the Set.GetTemplate to parse the templates, + // we let it to the jet template parser itself which does that at serve-time and caches each template by itself. + + return nil +} + +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. +func AddJetRuntimeVars(ctx context.Context, jetVarMap JetRuntimeVars) { + 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. +func (s *JetEngine) AddRuntimeVars(ctx context.Context, vars JetRuntimeVars) { + AddJetRuntimeVars(ctx, vars) +} + +type rangerAndRenderer struct { + ranger jet.Ranger + renderer jet.Renderer +} + +func (rr rangerAndRenderer) Range() (reflect.Value, reflect.Value, bool) { + return rr.ranger.Range() +} + +func (rr rangerAndRenderer) Render(jetRuntime *jet.Runtime) { + rr.renderer.Render(jetRuntime) +} + +// 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 + + if ctx, ok := w.(context.Context); ok { + runtimeVars := ctx.Values().Get(JetRuntimeVarsContextKey) + if runtimeVars != nil { + if jetVars, ok := runtimeVars.(JetRuntimeVars); ok { + vars = jetVars + } + } + } + + if ranger, ok := bindingData.(jet.Ranger); ok { + // Externally fixes a BUG on the jet template parser: + // eval.go#executeList(list *ListNode):NodeRange.isSet.getRanger(expression = st.evalPrimaryExpressionGroup) + // which does not act the "ranger" as element, instead is converted to a value of struct, which makes a jet.Ranger func(*myStruct) Range... + // not a compatible jet.Ranger. + // getRanger(st.context) should work but author of the jet library is not currently available, + // to allow a recommentation or a PR and I don't really want to vendor it because + // some end-users may use the jet import path to pass things like Global Funcs and etc. + // So to fix it (at least temporarily and only for ref Ranger) we ptr the ptr the "ranger", not the bindingData, and this may + // have its downside because the same bindingData may be compatible with other node actions like range or custom Render + // but we have no other way at the moment. The same problem exists on the `Renderer` too! + // The solution below fixes the above issue but any fields of the struct are not available, + // this is ok because most of the times if not always, the users of jet don't use fields on Ranger and custom Renderer inside the templates. + + if renderer, ok := bindingData.(jet.Renderer); ok { + // this can make a Ranger and Renderer both work together, unlike the jet parser itself. + return tmpl.Execute(w, vars, rangerAndRenderer{ranger, renderer}) + } + + return tmpl.Execute(w, vars, &ranger) + } + + if renderer, ok := bindingData.(jet.Renderer); ok { + // Here the fields are not available but usually if completes the jet.Renderer no + // fields are used in the template. + return tmpl.Execute(w, vars, &renderer) // see above ^. + } + + return tmpl.Execute(w, vars, bindingData) +}