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 }