diff --git a/HISTORY.md b/HISTORY.md
index 50a6764e..e930006a 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -364,6 +364,7 @@ Response:
Other Improvements:
+- Add a `ParseTemplate` method on view engines to manually parse and add a template from a text as [requested](https://github.com/kataras/iris/issues/1617). [Examples](https://github.com/kataras/iris/tree/master/_examples/view/parse-template).
- Full `http.FileSystem` interface support for all **view** engines as [requested](https://github.com/kataras/iris/issues/1575). The first argument of the functions(`HTML`, `Blocks`, `Pug`, `Amber`, `Ace`, `Jet`, `Django`, `Handlebars`) can now be either a directory of `string` type (like before) or a value which completes the `http.FileSystem` interface. The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string.
- Add `Route.ExcludeSitemap() *Route` to exclude a route from sitemap as requested in [chat](https://chat.iris-go.com), also offline routes are excluded automatically now.
diff --git a/_examples/README.md b/_examples/README.md
index 6c7346a3..f3a82530 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -110,19 +110,25 @@
* [Inject Engine Between Handlers](view/context-view-engine/main.go)
* [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go)
* [Write to a custom `io.Writer`](view/write-to)
- * [Parse a Template manually](view/parse-template/main.go)
+ * Parse a Template from Text
+ * [HTML, Pug and Ace](view/parse-parse/main.go)
+ * [Django](view/parse-parse/django/main.go)
+ * [Amber](view/parse-parse/amber/main.go)
+ * [Jet](view/parse-parse/jet/main.go)
+ * [Handlebars](view/parse-parse/handlebars/main.go)
* [Blocks](view/template_blocks_0)
* [Blocks Embedded](view/template_blocks_1_embedded)
* [Pug: `Actions`](view/template_pug_0)
* [Pug: `Includes`](view/template_pug_1)
* [Pug Embedded`](view/template_pug_2_embedded)
+ * [Ace](view/template_ace_0)
+ * [Django](view/template_django_0)
+ * [Amber](view/template_amber_0)
+ * [Amber Embedded](view/template_amber_1_embedded)
* [Jet](view/template_jet_0)
* [Jet Embedded](view/template_jet_1_embedded)
* [Jet 'urlpath' tmpl func](view/template_jet_2)
* [Jet Template Funcs from Struct](view/template_jet_3)
- * [Ace](view/template_ace_0)
- * [Amber](view/template_amber_0)
- * [Amber Embedded](view/template_amber_1_embedded)
* [Handlebars](view/template_handlebars_0)
* Third-Parties
* [Render `valyala/quicktemplate` templates](view/quicktemplate)
diff --git a/_examples/view/parse-template/amber/main.go b/_examples/view/parse-template/amber/main.go
new file mode 100644
index 00000000..ac1a5a80
--- /dev/null
+++ b/_examples/view/parse-template/amber/main.go
@@ -0,0 +1,28 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ e := iris.Amber(nil, ".amber") // You can still use a file system though.
+ e.AddFunc("greet", func(name string) string {
+ return "Hello, " + name + "!"
+ })
+ err := e.ParseTemplate("program.amber", []byte(`h1 #{ greet(Name) }`))
+ if err != nil {
+ panic(err)
+ }
+
+ app := iris.New()
+ app.RegisterView(e)
+ app.Get("/", index)
+
+ app.Listen(":8080")
+}
+
+func index(ctx iris.Context) {
+ ctx.View("program.amber", iris.Map{
+ "Name": "Gerasimos",
+ // Or per template:
+ // "greet": func(....)
+ })
+}
diff --git a/_examples/view/parse-template/django/main.go b/_examples/view/parse-template/django/main.go
new file mode 100644
index 00000000..aba041ab
--- /dev/null
+++ b/_examples/view/parse-template/django/main.go
@@ -0,0 +1,26 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ e := iris.Django(nil, ".html") // You can still use a file system though.
+ e.AddFunc("greet", func(name string) string {
+ return "Hello, " + name + "!"
+ })
+ err := e.ParseTemplate("program.html", []byte(`
{{greet(Name)}}
`))
+ if err != nil {
+ panic(err)
+ }
+
+ app := iris.New()
+ app.RegisterView(e)
+ app.Get("/", index)
+
+ app.Listen(":8080")
+}
+
+func index(ctx iris.Context) {
+ ctx.View("program.html", iris.Map{
+ "Name": "Gerasimos",
+ })
+}
diff --git a/_examples/view/parse-template/handlebars/main.go b/_examples/view/parse-template/handlebars/main.go
new file mode 100644
index 00000000..366b736f
--- /dev/null
+++ b/_examples/view/parse-template/handlebars/main.go
@@ -0,0 +1,24 @@
+package main
+
+import "github.com/kataras/iris/v12"
+
+func main() {
+ e := iris.Handlebars(nil, ".html") // You can still use a file system though.
+ e.ParseTemplate("program.html", `{{greet Name}}
`, iris.Map{
+ "greet": func(name string) string {
+ return "Hello, " + name + "!"
+ },
+ })
+
+ app := iris.New()
+ app.RegisterView(e)
+ app.Get("/", index)
+
+ app.Listen(":8080")
+}
+
+func index(ctx iris.Context) {
+ ctx.View("program.html", iris.Map{
+ "Name": "Gerasimos",
+ })
+}
diff --git a/_examples/view/parse-template/jet/main.go b/_examples/view/parse-template/jet/main.go
new file mode 100644
index 00000000..c70da265
--- /dev/null
+++ b/_examples/view/parse-template/jet/main.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+ "reflect"
+
+ "github.com/kataras/iris/v12"
+ "github.com/kataras/iris/v12/view"
+)
+
+func main() {
+ e := iris.Jet(nil, ".jet") // You can still use a file system though.
+ e.AddFunc("greet", func(args view.JetArguments) reflect.Value {
+ msg := "Hello, " + args.Get(0).String() + "!"
+ return reflect.ValueOf(msg)
+ })
+ err := e.ParseTemplate("program.jet", `{{greet(.Name)}}
`)
+ if err != nil {
+ panic(err)
+ }
+
+ app := iris.New()
+ app.RegisterView(e)
+ app.Get("/", index)
+
+ app.Listen(":8080")
+}
+
+func index(ctx iris.Context) {
+ ctx.View("program.jet", iris.Map{
+ "Name": "Gerasimos",
+ })
+}
diff --git a/_examples/view/parse-template/main.go b/_examples/view/parse-template/main.go
index bfddf399..1a45c4b8 100644
--- a/_examples/view/parse-template/main.go
+++ b/_examples/view/parse-template/main.go
@@ -1,22 +1,28 @@
// Package main shows how to parse a template through custom byte slice content.
+// The following works with HTML, Pug and Ace template parsers.
+// To learn how you can manually parse a template from a text for the rest
+// template parsers navigate through the example's subdirectories.
package main
import "github.com/kataras/iris/v12"
func main() {
- app := iris.New()
// To not load any templates from files or embedded data,
// pass nil or empty string on the first argument:
- // view := iris.HTML(nil, ".html")
+ // e := iris.HTML(nil, ".html")
- view := iris.HTML("./views", ".html")
- view.ParseTemplate("program.html", []byte(`{{greet .Name}}
`), iris.Map{
+ e := iris.HTML("./views", ".html")
+ // e := iris.Pug("./views",".pug")
+ // e := iris.Ace("./views",".ace")
+ e.ParseTemplate("program.html", []byte(`{{greet .Name}}
`), iris.Map{
"greet": func(name string) string {
return "Hello, " + name + "!"
},
})
- app.RegisterView(view)
+ app := iris.New()
+ app.RegisterView(e)
+
app.Get("/", index)
app.Get("/layout", layout)
diff --git a/_examples/view/template_handlebars_0/main.go b/_examples/view/template_handlebars_0/main.go
index 4978bae5..01033229 100644
--- a/_examples/view/template_handlebars_0/main.go
+++ b/_examples/view/template_handlebars_0/main.go
@@ -10,13 +10,13 @@ func main() {
app.Logger().SetLevel("debug")
// Init the handlebars engine
- engine := iris.Handlebars("./templates", ".html").Reload(true)
+ e := iris.Handlebars("./templates", ".html").Reload(true)
// Register a helper.
- engine.AddFunc("fullName", func(person map[string]string) string {
+ e.AddFunc("fullName", func(person map[string]string) string {
return person["firstName"] + " " + person["lastName"]
})
- app.RegisterView(engine)
+ app.RegisterView(e)
app.Get("/", func(ctx iris.Context) {
viewData := iris.Map{
diff --git a/view/amber.go b/view/amber.go
index b8c08acd..cca0f5ae 100644
--- a/view/amber.go
+++ b/view/amber.go
@@ -22,8 +22,9 @@ type AmberEngine struct {
reload bool
//
rmu sync.RWMutex // locks for `ExecuteWiter` when `reload` is true.
- funcs map[string]interface{}
templateCache map[string]*template.Template
+
+ Options amber.Options
}
var (
@@ -39,12 +40,17 @@ var (
// Amber(iris.Dir("./views"), ".amber") or
// Amber(AssetFile(), ".amber") for embedded data.
func Amber(fs interface{}, extension string) *AmberEngine {
+ fileSystem := getFS(fs)
s := &AmberEngine{
- fs: getFS(fs),
+ fs: fileSystem,
rootDir: "/",
extension: extension,
templateCache: make(map[string]*template.Template),
- funcs: make(map[string]interface{}),
+ Options: amber.Options{
+ PrettyPrint: false,
+ LineNumbers: false,
+ VirtualFilesystem: fileSystem,
+ },
}
return s
@@ -79,9 +85,16 @@ func (s *AmberEngine) Reload(developmentMode bool) *AmberEngine {
// - url func(routeName string, args ...string) string
// - urlpath func(routeName string, args ...string) string
// - render func(fullPartialName string) (template.HTML, error).
+//
+// Note that, Amber does not support functions per template,
+// instead it's using the "call" directive so any template-specific
+// functions should be passed using `Context.View/ViewData` binding data.
+// This method will modify the global amber's FuncMap which considers
+// as the "builtin" as this is the only way to actually add a function.
+// Note that, if you use more than one amber engine, the functions are shared.
func (s *AmberEngine) AddFunc(funcName string, funcBody interface{}) {
s.rmu.Lock()
- s.funcs[funcName] = funcBody
+ amber.FuncMap[funcName] = funcBody
s.rmu.Unlock()
}
@@ -90,28 +103,6 @@ func (s *AmberEngine) AddFunc(funcName string, funcBody interface{}) {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *AmberEngine) Load() error {
- s.rmu.Lock()
- defer s.rmu.Unlock()
-
- // prepare the global amber funcs
- funcs := template.FuncMap{}
-
- for k, v := range amber.FuncMap { // add the amber's default funcs
- funcs[k] = v
- }
-
- for k, v := range s.funcs {
- funcs[k] = v
- }
-
- amber.FuncMap = funcs // set the funcs
-
- opts := amber.Options{
- PrettyPrint: false,
- LineNumbers: false,
- VirtualFilesystem: s.fs,
- }
-
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
return nil
@@ -123,24 +114,66 @@ func (s *AmberEngine) Load() error {
}
}
- buf, err := asset(s.fs, path)
+ contents, err := asset(s.fs, path)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
- name := strings.TrimPrefix(path, "/")
-
- tmpl, err := amber.CompileData(buf, name, opts)
+ err = s.ParseTemplate(path, contents)
if err != nil {
- return fmt.Errorf("%s: %w", path, err)
+ return fmt.Errorf("%s: %v", path, err)
}
-
- s.templateCache[name] = tmpl
-
return nil
})
}
+// ParseTemplate adds a custom template from text.
+// This template parser does not support funcs per template directly.
+// Two ways to add a function:
+// Globally: Use `AddFunc` or pass them on `View` instead.
+// Per Template: Use `Context.ViewData/View`.
+func (s *AmberEngine) ParseTemplate(name string, contents []byte) error {
+ s.rmu.Lock()
+ defer s.rmu.Unlock()
+
+ comp := amber.New()
+ comp.Options = s.Options
+
+ err := comp.ParseData(contents, name)
+ if err != nil {
+ return err
+ }
+
+ data, err := comp.CompileString()
+ if err != nil {
+ return err
+ }
+
+ name = strings.TrimPrefix(name, "/")
+
+ /* Sadly, this does not work, only builtin amber.FuncMap
+ can be executed as function, the rest are compiled as data (prepends a "call"),
+ relative code:
+ https://github.com/eknkc/amber/blob/cdade1c073850f4ffc70a829e31235ea6892853b/compiler.go#L771-L794
+
+ tmpl := template.New(name).Funcs(amber.FuncMap).Funcs(s.funcs)
+ if len(funcs) > 0 {
+ tmpl.Funcs(funcs)
+ }
+
+ We can't add them as binding data of map type
+ because those data can be a struct by the caller and we don't want to messup.
+ */
+
+ tmpl := template.New(name).Funcs(amber.FuncMap)
+ _, err = tmpl.Parse(data)
+ if err == nil {
+ s.templateCache[name] = tmpl
+ }
+
+ return err
+}
+
func (s *AmberEngine) fromCache(relativeName string) *template.Template {
if s.reload {
s.rmu.RLock()
diff --git a/view/django.go b/view/django.go
index 495202d8..4204ce12 100644
--- a/view/django.go
+++ b/view/django.go
@@ -100,6 +100,7 @@ type DjangoEngine struct {
filters map[string]FilterFunction
// globals share context fields between templates.
globals map[string]interface{}
+ Set *pongo2.TemplateSet
templateCache map[string]*pongo2.Template
}
@@ -204,12 +205,6 @@ func (s *DjangoEngine) RegisterTag(tagName string, fn TagParser) error {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *DjangoEngine) Load() error {
- set := pongo2.NewSet("", &tDjangoAssetLoader{fs: s.fs, rootDir: s.rootDir})
- set.Globals = getPongoContext(s.globals)
-
- s.rmu.Lock()
- defer s.rmu.Unlock()
-
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, err error) error {
if info == nil || info.IsDir() {
return nil
@@ -221,17 +216,39 @@ func (s *DjangoEngine) Load() error {
}
}
- buf, err := asset(s.fs, path)
+ contents, err := asset(s.fs, path)
if err != nil {
return err
}
- name := strings.TrimPrefix(path, "/")
- s.templateCache[name], err = set.FromBytes(buf)
- return err
+ return s.ParseTemplate(path, contents)
})
}
+// ParseTemplate adds a custom template from text.
+// This parser does not support funcs per template. Use the `AddFunc` instead.
+func (s *DjangoEngine) ParseTemplate(name string, contents []byte) error {
+ s.rmu.Lock()
+ defer s.rmu.Unlock()
+
+ s.initSet()
+
+ name = strings.TrimPrefix(name, "/")
+ tmpl, err := s.Set.FromBytes(contents)
+ if err == nil {
+ s.templateCache[name] = tmpl
+ }
+
+ return err
+}
+
+func (s *DjangoEngine) initSet() { // protected by the caller.
+ if s.Set == nil {
+ s.Set = pongo2.NewSet("", &tDjangoAssetLoader{fs: s.fs, rootDir: s.rootDir})
+ s.Set.Globals = getPongoContext(s.globals)
+ }
+}
+
// getPongoContext returns the pongo2.Context from map[string]interface{} or from pongo2.Context, used internaly
func getPongoContext(templateData interface{}) pongo2.Context {
if templateData == nil {
diff --git a/view/handlebars.go b/view/handlebars.go
index 3c4017b3..4221d6b3 100644
--- a/view/handlebars.go
+++ b/view/handlebars.go
@@ -2,6 +2,7 @@ package view
import (
"fmt"
+ "html/template"
"io"
"net/http"
"os"
@@ -24,7 +25,7 @@ type HandlebarsEngine struct {
// parser configuration
layout string
rmu sync.RWMutex
- helpers map[string]interface{}
+ funcs template.FuncMap
templateCache map[string]*raymond.Template
}
@@ -46,7 +47,7 @@ func Handlebars(fs interface{}, extension string) *HandlebarsEngine {
rootDir: "/",
extension: extension,
templateCache: make(map[string]*raymond.Template),
- helpers: make(map[string]interface{}),
+ funcs: make(template.FuncMap), // global
}
// register the render helper here
@@ -94,14 +95,21 @@ func (s *HandlebarsEngine) Layout(layoutFile string) *HandlebarsEngine {
return s
}
-// AddFunc adds the function to the template's function map.
+// AddFunc adds a function to the templates.
// It is legal to overwrite elements of the default actions:
// - url func(routeName string, args ...string) string
// - urlpath func(routeName string, args ...string) string
// - render func(fullPartialName string) (raymond.HTML, error).
func (s *HandlebarsEngine) AddFunc(funcName string, funcBody interface{}) {
s.rmu.Lock()
- s.helpers[funcName] = funcBody
+ s.funcs[funcName] = funcBody
+ s.rmu.Unlock()
+}
+
+// AddGlobalFunc registers a global template function for all Handlebars view engines.
+func (s *HandlebarsEngine) AddGlobalFunc(funcName string, funcBody interface{}) {
+ s.rmu.Lock()
+ raymond.RegisterHelper(funcName, funcBody)
s.rmu.Unlock()
}
@@ -110,14 +118,6 @@ func (s *HandlebarsEngine) AddFunc(funcName string, funcBody interface{}) {
//
// Returns an error if something bad happens, user is responsible to catch it.
func (s *HandlebarsEngine) Load() error {
- s.rmu.Lock()
- defer s.rmu.Unlock()
-
- // register the global helpers on the first load
- if len(s.templateCache) == 0 && s.helpers != nil {
- raymond.RegisterHelpers(s.helpers)
- }
-
return walk(s.fs, s.rootDir, func(path string, info os.FileInfo, _ error) error {
if info == nil || info.IsDir() {
return nil
@@ -129,22 +129,37 @@ func (s *HandlebarsEngine) Load() error {
}
}
- buf, err := asset(s.fs, path)
+ contents, err := asset(s.fs, path)
if err != nil {
return err
}
-
- name := strings.TrimPrefix(path, "/")
- tmpl, err := raymond.Parse(string(buf))
- if err != nil {
- return err
- }
- s.templateCache[name] = tmpl
-
- return nil
+ return s.ParseTemplate(path, string(contents), nil)
})
}
+// ParseTemplate adds a custom template from text.
+func (s *HandlebarsEngine) ParseTemplate(name string, contents string, funcs template.FuncMap) error {
+ s.rmu.Lock()
+ defer s.rmu.Unlock()
+
+ name = strings.TrimPrefix(name, "/")
+ tmpl, err := raymond.Parse(contents)
+ if err == nil {
+ // Add functions for this template.
+ for k, v := range s.funcs {
+ tmpl.RegisterHelper(k, v)
+ }
+
+ for k, v := range funcs {
+ tmpl.RegisterHelper(k, v)
+ }
+
+ s.templateCache[name] = tmpl
+ }
+
+ return err
+}
+
func (s *HandlebarsEngine) fromCache(relativeName string) *raymond.Template {
if s.reload {
s.rmu.RLock()
@@ -201,7 +216,7 @@ func (s *HandlebarsEngine) ExecuteWriter(w io.Writer, filename string, layout st
context = make(map[string]interface{}, 1)
}
// I'm implemented the {{ yield }} as with the rest of template engines, so this is not inneed for iris, but the user can do that manually if want
- // there is no performanrce different: raymond.RegisterPartialTemplate(name, tmpl)
+ // there is no performance cost: raymond.RegisterPartialTemplate(name, tmpl)
context["yield"] = raymond.SafeString(contents)
}
diff --git a/view/html.go b/view/html.go
index d00b9601..837781c3 100644
--- a/view/html.go
+++ b/view/html.go
@@ -235,18 +235,8 @@ func (s *HTMLEngine) Load() error {
})
}
-func (s *HTMLEngine) initRootTmpl() { // protected by the caller.
- if s.Templates == nil {
- // the root template should be the same,
- // no matter how many reloads as the
- // following unexported fields cannot be modified.
- s.Templates = template.New(s.rootDir)
- s.Templates.Delims(s.left, s.right)
- }
-}
-
// ParseTemplate adds a custom template to the root template.
-func (s *HTMLEngine) ParseTemplate(name string, contents []byte, funcMap template.FuncMap) (err error) {
+func (s *HTMLEngine) ParseTemplate(name string, contents []byte, funcs template.FuncMap) (err error) {
s.rmu.Lock()
defer s.rmu.Unlock()
@@ -268,13 +258,23 @@ func (s *HTMLEngine) ParseTemplate(name string, contents []byte, funcMap templat
}
tmpl.Funcs(emptyFuncs).Funcs(s.funcs)
- if len(funcMap) > 0 {
- tmpl.Funcs(funcMap) // custom for this template.
+ if len(funcs) > 0 {
+ tmpl.Funcs(funcs) // custom for this template.
}
_, err = tmpl.Parse(text)
return
}
+func (s *HTMLEngine) initRootTmpl() { // protected by the caller.
+ if s.Templates == nil {
+ // the root template should be the same,
+ // no matter how many reloads as the
+ // following unexported fields cannot be modified.
+ s.Templates = template.New(s.rootDir)
+ s.Templates.Delims(s.left, s.right)
+ }
+}
+
func (s *HTMLEngine) executeTemplateBuf(name string, binding interface{}) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
err := s.Templates.ExecuteTemplate(buf, name, binding)
diff --git a/view/jet.go b/view/jet.go
index 7c309306..7db1b7b8 100644
--- a/view/jet.go
+++ b/view/jet.go
@@ -7,6 +7,7 @@ import (
"path/filepath"
"reflect"
"strings"
+ "sync"
"github.com/kataras/iris/v12/context"
@@ -28,6 +29,7 @@ type JetEngine struct {
// The Set is the `*jet.Set`, exported to offer any custom capabilities that jet users may want.
// Available after `Load`.
Set *jet.Set
+ mu sync.Mutex
// 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.
@@ -212,14 +214,7 @@ func (l *jetLoader) Exists(name string) (string, bool) {
// 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)
- }
- }
+ s.initSet()
// Note that, unlike the rest of template engines implementations,
// we don't call the Set.GetTemplate to parse the templates,
@@ -228,6 +223,30 @@ func (s *JetEngine) Load() error {
return nil
}
+// 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()
+
+ _, err := s.Set.LoadTemplate(name, contents)
+ return err
+}
+
+func (s *JetEngine) initSet() {
+ s.mu.Lock()
+ if s.Set == nil {
+ 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)
+ }
+ }
+ }
+ s.mu.Unlock()
+}
+
type (
// JetRuntimeVars is a type alias for `jet.VarMap`.
// Can be used at `AddJetRuntimeVars/JetEngine.AddRuntimeVars`