partials.go (7042B)
1 // Copyright 2017 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 // Package partials provides template functions for working with reusable
15 // templates.
16 package partials
17
18 import (
19 "context"
20 "errors"
21 "fmt"
22 "html/template"
23 "io"
24 "io/ioutil"
25 "reflect"
26 "strings"
27 "sync"
28 "time"
29
30 texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
31
32 "github.com/gohugoio/hugo/helpers"
33
34 "github.com/gohugoio/hugo/tpl"
35
36 bp "github.com/gohugoio/hugo/bufferpool"
37 "github.com/gohugoio/hugo/deps"
38 )
39
40 // TestTemplateProvider is global deps.ResourceProvider.
41 // NOTE: It's currently unused.
42 var TestTemplateProvider deps.ResourceProvider
43
44 type partialCacheKey struct {
45 name string
46 variant any
47 }
48
49 func (k partialCacheKey) templateName() string {
50 if !strings.HasPrefix(k.name, "partials/") {
51 return "partials/" + k.name
52 }
53 return k.name
54 }
55
56 // partialCache represents a cache of partials protected by a mutex.
57 type partialCache struct {
58 sync.RWMutex
59 p map[partialCacheKey]any
60 }
61
62 func (p *partialCache) clear() {
63 p.Lock()
64 defer p.Unlock()
65 p.p = make(map[partialCacheKey]any)
66 }
67
68 // New returns a new instance of the templates-namespaced template functions.
69 func New(deps *deps.Deps) *Namespace {
70 cache := &partialCache{p: make(map[partialCacheKey]any)}
71 deps.BuildStartListeners.Add(
72 func() {
73 cache.clear()
74 })
75
76 return &Namespace{
77 deps: deps,
78 cachedPartials: cache,
79 }
80 }
81
82 // Namespace provides template functions for the "templates" namespace.
83 type Namespace struct {
84 deps *deps.Deps
85 cachedPartials *partialCache
86 }
87
88 // contextWrapper makes room for a return value in a partial invocation.
89 type contextWrapper struct {
90 Arg any
91 Result any
92 }
93
94 // Set sets the return value and returns an empty string.
95 func (c *contextWrapper) Set(in any) string {
96 c.Result = in
97 return ""
98 }
99
100 // Include executes the named partial.
101 // If the partial contains a return statement, that value will be returned.
102 // Else, the rendered output will be returned:
103 // A string if the partial is a text/template, or template.HTML when html/template.
104 // Note that ctx is provided by Hugo, not the end user.
105 func (ns *Namespace) Include(ctx context.Context, name string, contextList ...any) (any, error) {
106 name, result, err := ns.include(ctx, name, contextList...)
107 if err != nil {
108 return result, err
109 }
110
111 if ns.deps.Metrics != nil {
112 ns.deps.Metrics.TrackValue(name, result, false)
113 }
114
115 return result, nil
116 }
117
118 // include is a helper function that lookups and executes the named partial.
119 // Returns the final template name and the rendered output.
120 func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) (string, any, error) {
121 var data any
122 if len(dataList) > 0 {
123 data = dataList[0]
124 }
125
126 var n string
127 if strings.HasPrefix(name, "partials/") {
128 n = name
129 } else {
130 n = "partials/" + name
131 }
132
133 templ, found := ns.deps.Tmpl().Lookup(n)
134 if !found {
135 // For legacy reasons.
136 templ, found = ns.deps.Tmpl().Lookup(n + ".html")
137 }
138
139 if !found {
140 return "", "", fmt.Errorf("partial %q not found", name)
141 }
142
143 var info tpl.ParseInfo
144 if ip, ok := templ.(tpl.Info); ok {
145 info = ip.ParseInfo()
146 }
147
148 var w io.Writer
149
150 if info.HasReturn {
151 // Wrap the context sent to the template to capture the return value.
152 // Note that the template is rewritten to make sure that the dot (".")
153 // and the $ variable points to Arg.
154 data = &contextWrapper{
155 Arg: data,
156 }
157
158 // We don't care about any template output.
159 w = ioutil.Discard
160 } else {
161 b := bp.GetBuffer()
162 defer bp.PutBuffer(b)
163 w = b
164 }
165
166 if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil {
167 return "", nil, err
168 }
169
170 var result any
171
172 if ctx, ok := data.(*contextWrapper); ok {
173 result = ctx.Result
174 } else if _, ok := templ.(*texttemplate.Template); ok {
175 result = w.(fmt.Stringer).String()
176 } else {
177 result = template.HTML(w.(fmt.Stringer).String())
178 }
179
180 return templ.Name(), result, nil
181 }
182
183 // IncludeCached executes and caches partial templates. The cache is created with name+variants as the key.
184 // Note that ctx is provided by Hugo, not the end user.
185 func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any, variants ...any) (any, error) {
186 key, err := createKey(name, variants...)
187 if err != nil {
188 return nil, err
189 }
190
191 result, err := ns.getOrCreate(ctx, key, context)
192 if err == errUnHashable {
193 // Try one more
194 key.variant = helpers.HashString(key.variant)
195 result, err = ns.getOrCreate(ctx, key, context)
196 }
197
198 return result, err
199 }
200
201 func createKey(name string, variants ...any) (partialCacheKey, error) {
202 var variant any
203
204 if len(variants) > 1 {
205 variant = helpers.HashString(variants...)
206 } else if len(variants) == 1 {
207 variant = variants[0]
208 t := reflect.TypeOf(variant)
209 switch t.Kind() {
210 // This isn't an exhaustive list of unhashable types.
211 // There may be structs with slices,
212 // but that should be very rare. We do recover from that situation
213 // below.
214 case reflect.Slice, reflect.Array, reflect.Map:
215 variant = helpers.HashString(variant)
216 }
217 }
218
219 return partialCacheKey{name: name, variant: variant}, nil
220 }
221
222 var errUnHashable = errors.New("unhashable")
223
224 func (ns *Namespace) getOrCreate(ctx context.Context, key partialCacheKey, context any) (result any, err error) {
225 start := time.Now()
226 defer func() {
227 if r := recover(); r != nil {
228 err = r.(error)
229 if strings.Contains(err.Error(), "unhashable type") {
230 ns.cachedPartials.RUnlock()
231 err = errUnHashable
232 }
233 }
234 }()
235
236 ns.cachedPartials.RLock()
237 p, ok := ns.cachedPartials.p[key]
238 ns.cachedPartials.RUnlock()
239
240 if ok {
241 if ns.deps.Metrics != nil {
242 ns.deps.Metrics.TrackValue(key.templateName(), p, true)
243 // The templates that gets executed is measured in Execute.
244 // We need to track the time spent in the cache to
245 // get the totals correct.
246 ns.deps.Metrics.MeasureSince(key.templateName(), start)
247
248 }
249 return p, nil
250 }
251
252 // This needs to be done outside the lock.
253 // See #9588
254 _, p, err = ns.include(ctx, key.name, context)
255 if err != nil {
256 return nil, err
257 }
258
259 ns.cachedPartials.Lock()
260 defer ns.cachedPartials.Unlock()
261 // Double-check.
262 if p2, ok := ns.cachedPartials.p[key]; ok {
263 if ns.deps.Metrics != nil {
264 ns.deps.Metrics.TrackValue(key.templateName(), p, true)
265 ns.deps.Metrics.MeasureSince(key.templateName(), start)
266 }
267 return p2, nil
268
269 }
270 if ns.deps.Metrics != nil {
271 ns.deps.Metrics.TrackValue(key.templateName(), p, false)
272 }
273
274 ns.cachedPartials.p[key] = p
275
276 return p, nil
277 }