forked from gobuffalo/plush
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplush.go
More file actions
347 lines (305 loc) · 9.38 KB
/
Copy pathplush.go
File metadata and controls
347 lines (305 loc) · 9.38 KB
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
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package plush
import (
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gobuffalo/plush/v5/helpers/hctx"
"github.com/gobuffalo/plush/v5/helpers/meta"
)
// DefaultTimeFormat is the default way of formatting a time.Time type.
// This a **GLOBAL** variable, so if you change it, it will change for
// templates rendered through the `plush` package. If you want to set a
// specific time format for a particular call to `Render` you can set
// the `TIME_FORMAT` in the context.
//
/*
ctx.Set("TIME_FORMAT", "2006-02-Jan")
s, err = Render(input, ctx)
*/
var DefaultTimeFormat = "January 02, 2006 15:04:05 -0700"
var PunchHoleCacheLifetime = 1 * time.Minute
var cacheEnabled bool
var holeTemplateFileKey = "__plush_internal_hole_render_key_" + fmt.Sprintf("%d", time.Now().UnixNano()) + "__"
var errClearCache error = errors.New("template recently cached, skipping")
var templateCacheBackend TemplateCache
func PlushCacheSetup(ts TemplateCache) {
cacheEnabled = true
templateCacheBackend = ts
}
// BuffaloRenderer implements the render.TemplateEngine interface allowing velvet to be used as a template engine
// for Buffalo
func BuffaloRenderer(input string, data map[string]interface{}, helpers map[string]interface{}) (string, error) {
if data == nil {
data = make(map[string]interface{})
}
for k, v := range helpers {
data[k] = v
}
ctx := NewContextWith(data)
defer func() {
if data != nil {
for k := range ctx.data.localInterner.stringToID {
data[k] = ctx.Value(k)
}
}
}()
return Render(input, ctx)
}
// Parse an input string and return a Template, and caches the parsed template.
func Parse(input ...string) (*Template, error) {
if templateCacheBackend == nil || !cacheEnabled || len(input) == 1 || len(input) > 2 {
return NewTemplate(input[0])
}
filename := input[1]
var astKey string
isPlushFile := isFilePlush(filename)
if isPlushFile {
astKey = GenerateASTKey(filename)
}
if filename != "" && templateCacheBackend != nil && isPlushFile {
t, ok := templateCacheBackend.Get(astKey)
if ok {
cloned := &Template{
Program: t.Program,
IsCache: true,
}
return cloned, nil
}
}
t, err := NewTemplate(input[0])
if err != nil {
return t, err
}
// Cache the AST
if cacheEnabled && templateCacheBackend != nil && filename != "" && isPlushFile {
astTemplate := &Template{
Program: t.Program,
IsCache: false,
}
templateCacheBackend.Set(astKey, astTemplate)
}
return t, nil
}
// RenderWithBudget renders a template and enforces a work-unit limit.
// Returns ErrBudgetExceeded if the template exhausts the budget.
// Existing Render() is completely unchanged.
func RenderWithBudget(input string, limit int64, ctx *Context) (string, error) {
b := NewBudget(limit)
ctx.WithBudget(b)
return Render(input, ctx)
}
// RenderWithBudgetConfig renders with a fully custom cost configuration.
func RenderWithBudgetConfig(input string, limit int64, costs BudgetCosts, ctx *Context) (string, error) {
b := NewBudgetWithCosts(limit, costs)
ctx.WithBudget(b)
return Render(input, ctx)
}
func isHole(ctx hctx.Context) bool {
if ctx.Value(holeTemplateFileKey) == nil {
return false
}
if ctx.Value(meta.TemplateFileKey) == nil {
return false
}
ss, _ := ctx.Value(holeTemplateFileKey).(string)
ss2, _ := ctx.Value(meta.TemplateFileKey).(string)
return ss2 == ss
}
// Render a string using the given context.
func Render(input string, ctx hctx.Context) (string, error) {
var filename string
// Extract filename from context if we're not in a hole rendering pass.
// The filename is used for template caching - only main templates (not holes) should use cache.
if !isHole(ctx) && ctx.Value(meta.TemplateFileKey) != nil {
if rawFilename, ok := ctx.Value(meta.TemplateFileKey).(string); ok {
filename = cleanFilePath(rawFilename) // ✅ Clean once here
}
}
forceCacheClear := false
// Try to render from cache if conditions are met:
// - Not in hole rendering pass (prevents infinite recursion)
// - Cache is enabled and backend is available
// - Template has a filename for cache key
if !isHole(ctx) && filename != "" {
cacheT, cacheErr := renderFromCache(filename, ctx)
if cacheErr == nil {
return cacheT, nil
} else if cacheErr == errClearCache {
forceCacheClear = true
}
}
t, err := Parse(input, filename)
if err != nil {
return "", err
}
isPlushFile := isFilePlush(filename)
// Execute template to get skeleton with hole markers
s, holeMarkers, err := t.Exec(ctx)
if err != nil {
return "", err
}
if !isPlushFile {
return s, err
}
//Don't bloat the cache with Skeletons that have no holes
// If there are holes, store skeleton and hole markers in the template for future use
// This is used when caching the fully rendered template with holes filled in
if len(holeMarkers) > 0 {
t.Skeleton = s
t.PunchHole = holeMarkers
}
if (!t.IsCache || forceCacheClear) && cacheEnabled {
defer func() {
if templateCacheBackend != nil && filename != "" && isPlushFile && len(holeMarkers) > 0 {
fullKey := generateFullKey(filename, ctx)
cacheableTemplate := &Template{
Skeleton: t.Skeleton,
PunchHole: holesCopy(t.PunchHole),
IsCache: false,
LastCached: time.Now(),
}
templateCacheBackend.Set(fullKey, cacheableTemplate)
}
}()
}
// If we have holes and this is the main render pass (not hole rendering),
// render holes concurrently and fill them into the skeleton
if !isHole(ctx) && len(t.PunchHole) > 0 {
hc := renderHolesConcurrently(t.PunchHole, ctx)
return fillHoles(s, hc)
}
// Return skeleton as-is (either no holes or we're in hole rendering pass)
return s, nil
}
// fillHoles replaces all markers in the rendered string with their rendered content using stored positions.
func fillHoles(rendered string, holes []HoleMarker) (string, error) {
var sb strings.Builder
last := 0
for _, pos := range holes {
if pos.err != nil {
return "", pos.err
}
sb.WriteString(rendered[last:pos.start])
sb.WriteString(pos.content)
last = pos.end
}
sb.WriteString(rendered[last:])
return sb.String(), nil
}
func renderHolesConcurrently(holes []HoleMarker, ctx hctx.Context) []HoleMarker {
if len(holes) == 0 {
return holes
}
var wg sync.WaitGroup
wg.Add(len(holes))
holeCtx := ctx.New()
octx := holeCtx.(*Context)
defer func() {
ctx = octx
}()
var currentfileName string
if holeCtx.Value(meta.TemplateFileKey) != nil {
tempF, _ := holeCtx.Value(meta.TemplateFileKey).(string)
if tempF != "" {
currentfileName = filepath.Base(tempF)
}
}
holeCtx.Set(holeTemplateFileKey, holeCtx.Value(meta.TemplateFileKey))
for k, hole := range holes {
go func(k int, childCtx hctx.Context, h HoleMarker) {
defer wg.Done()
content, err := Render(h.input, childCtx)
if err != nil {
content = err.Error() + " in " + currentfileName
}
holes[k].content = content
}(k, holeCtx.New(), hole)
}
wg.Wait()
return holes
}
func RenderR(input io.Reader, ctx hctx.Context) (string, error) {
b, err := ioutil.ReadAll(input)
if err != nil {
return "", err
}
return Render(string(b), ctx)
}
// RunScript allows for "pure" plush scripts to be executed.
func RunScript(input string, ctx hctx.Context) error {
input = "<% " + input + "%>"
ctx = ctx.New()
ctx.Set("print", func(i interface{}) {
fmt.Print(i)
})
ctx.Set("println", func(i interface{}) {
fmt.Println(i)
})
_, err := Render(input, ctx)
return err
}
type interfaceable interface {
Interface() interface{}
}
// HTMLer generates HTML source
type HTMLer interface {
HTML() template.HTML
}
func holesCopy(holes []HoleMarker) []HoleMarker {
holesCopy := make([]HoleMarker, len(holes))
for i := range holes {
holesCopy[i] = holes[i]
holesCopy[i].content = ""
holesCopy[i].err = nil
}
return holesCopy
}
// This function tries to get the template from the cache if it exists
// It only works if we are not in a hole rendering pass, and if we have a filename
// and if cache is enabled and if we have a cache backend
// If we are in a hole rendering pass, we should not use the cache.
// If we are in the first pass, and there is a filename, we should use the cache.
// If there is no filename, we should not use the cache.
// If cache is disabled, we should not use the cache.
// If there is no templateCacheBackend, we should not use the cache.
func renderFromCache(filename string, ctx hctx.Context) (string, error) {
if filename == "" || !cacheEnabled || templateCacheBackend == nil || isHole(ctx) {
return "", errors.New("cache not available")
}
astKey := GenerateASTKey(filename)
_, astExists := templateCacheBackend.Get(astKey)
if !astExists {
return "", errors.New("AST not cached")
}
fullKey := generateFullKey(filename, ctx)
inCacheTemplate, inCache := templateCacheBackend.Get(fullKey)
if inCache &&
inCacheTemplate != nil &&
inCacheTemplate.Skeleton != "" &&
len(inCacheTemplate.PunchHole) > 0 {
if time.Since(inCacheTemplate.LastCached) > PunchHoleCacheLifetime {
return "", errClearCache
}
hc := holesCopy(inCacheTemplate.PunchHole)
hc = renderHolesConcurrently(hc, ctx)
return fillHoles(inCacheTemplate.Skeleton, hc)
}
return "", errors.New("no cached template found")
}
func isFilePlush(filename string) bool {
if len(filename) < 6 {
return false
}
// Check for .plush.html first (longer suffix)
if len(filename) >= 11 && filename[len(filename)-11:] == ".plush.html" {
return true
}
// Check for .plush
return filename[len(filename)-6:] == ".plush"
}