Skip to content

Commit

Permalink
Add a 'ParseTemplate' method on view engines to manually parse and ad…
Browse files Browse the repository at this point in the history
  • Loading branch information
kataras committed Sep 8, 2020
1 parent 64038b0 commit a4996b9
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 96 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions _examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions _examples/view/parse-template/amber/main.go
Original file line number Diff line number Diff line change
@@ -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(....)
})
}
26 changes: 26 additions & 0 deletions _examples/view/parse-template/django/main.go
Original file line number Diff line number Diff line change
@@ -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(`<h1>{{greet(Name)}}</h1>`))
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",
})
}
24 changes: 24 additions & 0 deletions _examples/view/parse-template/handlebars/main.go
Original file line number Diff line number Diff line change
@@ -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", `<h1>{{greet Name}}</h1>`, 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",
})
}
32 changes: 32 additions & 0 deletions _examples/view/parse-template/jet/main.go
Original file line number Diff line number Diff line change
@@ -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", `<h1>{{greet(.Name)}}</h1>`)
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",
})
}
16 changes: 11 additions & 5 deletions _examples/view/parse-template/main.go
Original file line number Diff line number Diff line change
@@ -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(`<h1>{{greet .Name}}</h1>`), iris.Map{
e := iris.HTML("./views", ".html")
// e := iris.Pug("./views",".pug")
// e := iris.Ace("./views",".ace")
e.ParseTemplate("program.html", []byte(`<h1>{{greet .Name}}</h1>`), 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)

Expand Down
6 changes: 3 additions & 3 deletions _examples/view/template_handlebars_0/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
99 changes: 66 additions & 33 deletions view/amber.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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()
}

Expand All @@ -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
Expand All @@ -123,22 +114,64 @@ 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)
}
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 nil
})
return err
}

func (s *AmberEngine) fromCache(relativeName string) *template.Template {
Expand Down
Loading

0 comments on commit a4996b9

Please sign in to comment.