forked from kataras/iris
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtemplate.go
301 lines (259 loc) · 9.53 KB
/
template.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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.Response.Header.Add("Content-Encoding", "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
}