iris/template.go

303 lines
9.6 KiB
Go

package iris
import (
"io"
"path/filepath"
"github.com/iris-contrib/errors"
"github.com/kataras/iris/utils"
)
var (
builtinFuncs = [...]string{"url", "urlpath"}
// DefaultTemplateDirectory the default directory if empty setted
DefaultTemplateDirectory = "." + utils.PathSeparator + "templates"
)
const (
// DefaultTemplateExtension the default file extension if empty setted
DefaultTemplateExtension = ".html"
// NoLayout to disable layout for a particular template file
NoLayout = "@.|.@iris_no_layout@.|.@"
// TemplateLayoutContextKey is the name of the user values which can be used to set a template layout from a middleware and override the parent's
TemplateLayoutContextKey = "templateLayout"
)
type (
// TemplateEngine the interface that all template engines must implement
TemplateEngine interface {
// LoadDirectory builds the templates, usually by directory and extension but these are engine's decisions
LoadDirectory(directory string, extension string) error
// LoadAssets loads the templates by binary
// assetFn is a func which returns bytes, use it to load the templates by binary
// namesFn returns the template filenames
LoadAssets(virtualDirectory string, virtualExtension string, assetFn func(name string) ([]byte, error), namesFn func() []string) error
// ExecuteWriter finds, execute a template and write its result to the out writer
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout" or "gzip" option
ExecuteWriter(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) error
}
// TemplateEngineFuncs is optional interface for the TemplateEngine
// used to insert the Iris' standard funcs, see var 'usedFuncs'
TemplateEngineFuncs interface {
// Funcs should returns the context or the funcs,
// this property is used in order to register the iris' helper funcs
Funcs() map[string]interface{}
}
)
type (
// TemplateFuncs is is a helper type for map[string]interface{}
TemplateFuncs map[string]interface{}
// RenderOptions is a helper type for the optional runtime options can be passed by user when Render
// an example of this is the "layout" or "gzip" option
// same as Map but more specific name
RenderOptions map[string]interface{}
)
// IsFree returns true if a function can be inserted to this map
// return false if this key is already used by Iris
func (t TemplateFuncs) IsFree(key string) bool {
for i := range builtinFuncs {
if builtinFuncs[i] == key {
return false
}
}
return true
}
func getGzipOption(ctx *Context, options map[string]interface{}) bool {
gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options.
if b, isBool := gzipOpt.(bool); isBool {
return b
}
return ctx.framework.Config.Gzip
}
func getCharsetOption(options map[string]interface{}) string {
charsetOpt := options["charset"]
if s, isString := charsetOpt.(string); isString {
return s
}
return "" // we return empty in order to set the default charset if not founded.
}
type (
// TemplateEngineLocation contains the funcs to set the location for the templates by directory or by binary
TemplateEngineLocation struct {
directory string
extension string
assetFn func(name string) ([]byte, error)
namesFn func() []string
}
// TemplateEngineBinaryLocation called after TemplateEngineLocation's Directory, used when files are distrubuted inside the app executable
TemplateEngineBinaryLocation struct {
location *TemplateEngineLocation
}
)
// Directory sets the directory to load from
// returns the Binary location which is optional
func (t *TemplateEngineLocation) Directory(dir string, fileExtension string) *TemplateEngineBinaryLocation {
if dir == "" {
dir = DefaultTemplateDirectory // the default templates dir
}
if fileExtension == "" {
fileExtension = DefaultTemplateExtension
} else if fileExtension[0] != '.' { // if missing the start dot
fileExtension = "." + fileExtension
}
t.directory = dir
t.extension = fileExtension
return &TemplateEngineBinaryLocation{location: t}
}
// Binary sets the asset(s) and asssets names to load from, works with Directory
func (t *TemplateEngineBinaryLocation) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) {
if assetFn == nil || namesFn == nil {
return
}
t.location.assetFn = assetFn
t.location.namesFn = namesFn
// if extension is not static(setted by .Directory)
if t.location.extension == "" {
if names := namesFn(); len(names) > 0 {
t.location.extension = filepath.Ext(names[0]) // we need the extension to get the correct template engine on the Render method
}
}
}
func (t *TemplateEngineLocation) isBinary() bool {
return t.assetFn != nil && t.namesFn != nil
}
// templateEngineWrapper is the wrapper of a template engine
type templateEngineWrapper struct {
TemplateEngine
location *TemplateEngineLocation
buffer *utils.BufferPool
reload bool
}
var (
errMissingDirectoryOrAssets = errors.New("Missing Directory or Assets by binary for the template engine!")
errNoTemplateEngineForExt = errors.New("No template engine found to manage '%s' extensions")
)
func (t *templateEngineWrapper) load() error {
if t.location.isBinary() {
t.LoadAssets(t.location.directory, t.location.extension, t.location.assetFn, t.location.namesFn)
} else if t.location.directory != "" {
t.LoadDirectory(t.location.directory, t.location.extension)
} else {
return errMissingDirectoryOrAssets.Return()
}
return nil
}
// execute execute a template and write its result to the context's body
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout"
// note that gzip option is an iris dynamic option which exists for all template engines
// the gzip and charset options are built'n with iris
func (t *templateEngineWrapper) execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and templateEngines. getBy
return errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
if err = t.load(); err != nil {
return
}
}
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := ctx.framework.Config.Gzip
charset := ctx.framework.Config.Charset
if len(options) > 0 {
gzipEnabled = getGzipOption(ctx, options[0])
if chs := getCharsetOption(options[0]); chs != "" {
charset = chs
}
}
ctxLayout := ctx.GetString(TemplateLayoutContextKey)
if ctxLayout != "" {
if len(options) > 0 {
options[0]["layout"] = ctxLayout
} else {
options = []map[string]interface{}{map[string]interface{}{"layout": ctxLayout}}
}
}
ctx.SetContentType(contentHTML + "; charset=" + charset)
var out io.Writer
if gzipEnabled && ctx.clientAllowsGzip() {
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter())
defer ctx.framework.ReleaseGzip(gzipWriter)
out = gzipWriter
} else {
out = ctx.Response.BodyWriter()
}
err = t.ExecuteWriter(out, filename, binding, options...)
return err
}
// executeToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders
func (t *templateEngineWrapper) executeToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and templateEngines. getBy
return "", errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
if err = t.load(); err != nil {
return
}
}
out := t.buffer.Get()
defer t.buffer.Put(out)
err = t.ExecuteWriter(out, filename, binding, opt...)
if err == nil {
result = out.String()
}
return
}
// templateEngines is the container and manager of the template engines
type templateEngines struct {
helpers map[string]interface{}
engines []*templateEngineWrapper
reload bool
}
// getBy receives a filename, gets its extension and returns the template engine responsible for that file extension
func (t *templateEngines) getBy(filename string) *templateEngineWrapper {
extension := filepath.Ext(filename)
for i, n := 0, len(t.engines); i < n; i++ {
e := t.engines[i]
if e.location.extension == extension {
return e
}
}
return nil
}
// add adds but not loads a template engine
func (t *templateEngines) add(e TemplateEngine) *TemplateEngineLocation {
location := &TemplateEngineLocation{}
// add the iris helper funcs
if funcer, ok := e.(TemplateEngineFuncs); ok {
if funcer.Funcs() != nil {
for k, v := range t.helpers {
funcer.Funcs()[k] = v
}
}
}
tmplEngine := &templateEngineWrapper{
TemplateEngine: e,
location: location,
buffer: utils.NewBufferPool(8),
reload: t.reload,
}
t.engines = append(t.engines, tmplEngine)
return location
}
// loadAll loads all templates using all template engines, returns the first error
// called on iris' initialize
func (t *templateEngines) loadAll() error {
for i, n := 0, len(t.engines); i < n; i++ {
e := t.engines[i]
if e.location.directory == "" {
e.location.directory = DefaultTemplateDirectory // the defualt dir ./templates
}
if e.location.extension == "" {
e.location.extension = DefaultTemplateExtension // the default file ext .html
}
e.reload = t.reload // update the configuration every time a load happens
if err := e.load(); err != nil {
return err
}
}
return nil
}