-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathengine.go
253 lines (220 loc) · 6.95 KB
/
engine.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
package engine
import (
"bytes"
"errors"
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/sprig"
)
var (
// NoTemplateFound indicates that a desired template cannot be located.
NoTemplateFound = errors.New("no template found")
// NoAssetFound indicates that no asset could be loaded.
NoAssetFound = errors.New("no asset found")
// IllegalName indicates that a name contains illegal characters or patterns.
IllegalName = errors.New("name contains illegal patterns")
)
var NamedTemplateSeparator = "#"
// New creates a new *Template and processes the given directories.
//
// Each path should point to a "theme" directory. That directory is scanned
// for templates, which are compiled immediately and then cached.
//
// Paths are normalized, but the resulting normalized version cannot have
// a relative path. So the path "foo/bar/.." is fine, and will evaluate to
// "foo", but the path "foo/../.." is not okay, as it will evaluate to
// "..", which represents a potential security risk.
//
// Each path is scanned for files that end with the extension '.tpl'.
// Directories are not scanned recursively. Any other files or directories are
// ignored.
//
// For convenience, the engine supports an additional set of template
// functions as defined in Sprig:
// https://github.com/Masterminds/sprig
// These can be disabled by not passing the Sprig functions into NewEngine.
func New(paths ...string) (*Engine, error) {
return NewEngine(paths, sprig.FuncMap(), []string{})
}
// NewEngine constructs a new *Engine.
// NewEngine provides more control over the template engine than New.
//
// - funcMap is passed to the template.
// - options are passed to the template.
func NewEngine(paths []string, funcs template.FuncMap, options []string) (*Engine, error) {
// First, we do a quick normalization of all paths.
for i, d := range paths {
d = filepath.Clean(d)
// Clean will resolve '..' when possible. If any are left, it means the
// relative path is above the given directory, and that's a security
// problem.
if !legalName(d) {
return nil, IllegalName
}
if !dirExists(d) {
return nil, fmt.Errorf("could not read directory '%s'", d)
}
paths[i] = d
}
e := &Engine{
dirs: paths,
cache: make(map[string]map[string]bool, len(paths)),
master: template.New("master"),
}
if len(funcs) > 0 {
e.master.Funcs(funcs)
}
if len(options) > 0 {
e.master.Option(options...)
}
return e, e.parse()
}
type Engine struct {
// Order is important, so we keep dirs to maintain an ordering of themes.
dirs []string
cache map[string]map[string]bool
master *template.Template
}
// Render looks for a template with the given name, then executes it with the given data.
//
// The 'name' parameter should be a relative template name (foo.tpl). This
// will look through all of the known templates and execute the first match
// found. Traversal order is the order in which the templates were added.
//
// The 'data' will be passed into the template unaltered.
//
// If the renderer cannot find a template, it returns NoTemplateFound. If
// the template cannot be rendered, it may return a different error.
func (e *Engine) Render(name string, data interface{}) (string, error) {
var buf bytes.Buffer
// Support explicitly named templates (things from a template
// define) by accessing them directly.
if strings.HasPrefix(name, NamedTemplateSeparator) {
err := e.master.ExecuteTemplate(&buf, name[1:], data)
return buf.String(), err
}
// File-based templates.
n := filepath.Clean(name)
for _, d := range e.dirs {
if t, ok := e.cache[d][n]; ok && t {
key := filepath.Join(d, n)
err := e.master.ExecuteTemplate(&buf, key, data)
return buf.String(), err
}
}
return "", NoTemplateFound
}
// Asset returns the first matching asset path.
//
// An asset is a non-template file or directory in a theme directory. This
// function returns the string path of the first path that matches.
//
// An asset path is only returned if the asset exists and can be stat'ed.
func (e *Engine) Asset(name string) (string, error) {
name = filepath.Clean(name)
if !legalName(name) {
return "", IllegalName
}
// XXX: Should we allow .tpl files to be fetched as assets? Probably
// not. For now, denying.
if filepath.Ext(name) == ".tpl" {
return "", IllegalName
}
for _, d := range e.dirs {
p := filepath.Join(d, name)
if _, err := os.Stat(p); err == nil {
return p, nil
}
}
return "", NoAssetFound
}
// Dirs returns a list of directories that this Engine knows about.
//
// Directories are presented in their cleaned, but not absolute, form.
func (e *Engine) Dirs() []string {
return e.dirs
}
// Paths returns all know template paths.
func (e *Engine) Paths() []string {
res := make([]string, 0, len(e.dirs))
for base, tt := range e.cache {
for rel, _ := range tt {
res = append(res, filepath.Join(base, rel))
}
}
return res
}
func dirExists(d string) bool {
fi, err := os.Stat(d)
if err != nil {
return false
}
return fi.IsDir()
}
func legalName(d string) bool {
// I'm not sure how OS-independent this would turn out to be.
// An alternative test might be to call Abs and make sure that this
// path is "present" in Abs. That, however, would render "." illegal.
return !strings.Contains(d, "..")
}
func clean(d string) string {
return filepath.Clean(d)
}
func (e *Engine) parse() error {
// XXX: It is assumed that e.dirs have already been normalized and
// checked.
for _, d := range e.dirs {
ts := filepath.Join(d, "*.tpl")
files, err := filepath.Glob(ts)
if err != nil {
// ErrBadPattern is the only error that
// will return. files is nil if the pattern didn't turn up
// anything.
return err
}
// An dir with no templates is totally legit. This directory may
// just contain other assets. So we add to the map and continue.
if files == nil {
e.cache[d] = map[string]bool{}
continue
}
e.cache[d] = make(map[string]bool, len(files))
for _, f := range files {
// Second half of cache key.
r, err := filepath.Rel(d, f)
if err != nil {
return err
}
// TODO: Reading the file and then casting it to a string
// doesn't feel like the right solution. But using ParseFiles
// creates its own naming scheme, which doesn't work for us.
data, err := ioutil.ReadFile(f)
if err != nil {
return err
}
// Assumption is that f is exactly the same as filepath.Join(d, r)
if newt, err := e.master.New(f).Parse(string(data)); err != nil {
return err
} else {
// TODO: Currently, these entries are unused, since we
// access globally defined named templates directly. But
// this provides useful debugging information about where
// a particular global template is defined.
for _, tpl := range newt.Templates() {
tname := tpl.Name()
// Skip the ones we already know about.
if tname == "master" || tname == f {
continue
}
e.cache[d][r+NamedTemplateSeparator+tname] = true
}
}
e.cache[d][r] = true
}
}
return nil
}