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`