github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/tpl/partials/partials.go (about) 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 "errors" 20 "fmt" 21 "html/template" 22 "io" 23 "io/ioutil" 24 "reflect" 25 "strings" 26 "sync" 27 28 "github.com/gohugoio/hugo/common/hreflect" 29 texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" 30 31 "github.com/gohugoio/hugo/helpers" 32 33 "github.com/gohugoio/hugo/tpl" 34 35 bp "github.com/gohugoio/hugo/bufferpool" 36 "github.com/gohugoio/hugo/deps" 37 ) 38 39 // TestTemplateProvider is global deps.ResourceProvider. 40 // NOTE: It's currently unused. 41 var TestTemplateProvider deps.ResourceProvider 42 43 type partialCacheKey struct { 44 name string 45 variant interface{} 46 } 47 48 // partialCache represents a cache of partials protected by a mutex. 49 type partialCache struct { 50 sync.RWMutex 51 p map[partialCacheKey]interface{} 52 } 53 54 func (p *partialCache) clear() { 55 p.Lock() 56 defer p.Unlock() 57 p.p = make(map[partialCacheKey]interface{}) 58 } 59 60 // New returns a new instance of the templates-namespaced template functions. 61 func New(deps *deps.Deps) *Namespace { 62 cache := &partialCache{p: make(map[partialCacheKey]interface{})} 63 deps.BuildStartListeners.Add( 64 func() { 65 cache.clear() 66 }) 67 68 return &Namespace{ 69 deps: deps, 70 cachedPartials: cache, 71 } 72 } 73 74 // Namespace provides template functions for the "templates" namespace. 75 type Namespace struct { 76 deps *deps.Deps 77 cachedPartials *partialCache 78 } 79 80 // contextWrapper makes room for a return value in a partial invocation. 81 type contextWrapper struct { 82 Arg interface{} 83 Result interface{} 84 } 85 86 // Set sets the return value and returns an empty string. 87 func (c *contextWrapper) Set(in interface{}) string { 88 c.Result = in 89 return "" 90 } 91 92 // Include executes the named partial. 93 // If the partial contains a return statement, that value will be returned. 94 // Else, the rendered output will be returned: 95 // A string if the partial is a text/template, or template.HTML when html/template. 96 func (ns *Namespace) Include(name string, contextList ...interface{}) (interface{}, error) { 97 name = strings.TrimPrefix(name, "partials/") 98 99 var context interface{} 100 if len(contextList) > 0 { 101 context = contextList[0] 102 } 103 104 n := "partials/" + name 105 templ, found := ns.deps.Tmpl().Lookup(n) 106 107 if !found { 108 // For legacy reasons. 109 templ, found = ns.deps.Tmpl().Lookup(n + ".html") 110 } 111 112 if !found { 113 return "", fmt.Errorf("partial %q not found", name) 114 } 115 116 var info tpl.ParseInfo 117 if ip, ok := templ.(tpl.Info); ok { 118 info = ip.ParseInfo() 119 } 120 121 var w io.Writer 122 123 if info.HasReturn { 124 if !hreflect.IsTruthful(context) { 125 // TODO(bep) we need to fix this, but it is non-trivial. 126 return nil, errors.New("partial that returns a value needs a non-zero argument.") 127 } 128 // Wrap the context sent to the template to capture the return value. 129 // Note that the template is rewritten to make sure that the dot (".") 130 // and the $ variable points to Arg. 131 context = &contextWrapper{ 132 Arg: context, 133 } 134 135 // We don't care about any template output. 136 w = ioutil.Discard 137 } else { 138 b := bp.GetBuffer() 139 defer bp.PutBuffer(b) 140 w = b 141 } 142 143 if err := ns.deps.Tmpl().Execute(templ, w, context); err != nil { 144 return "", err 145 } 146 147 var result interface{} 148 149 if ctx, ok := context.(*contextWrapper); ok { 150 result = ctx.Result 151 } else if _, ok := templ.(*texttemplate.Template); ok { 152 result = w.(fmt.Stringer).String() 153 } else { 154 result = template.HTML(w.(fmt.Stringer).String()) 155 } 156 157 if ns.deps.Metrics != nil { 158 ns.deps.Metrics.TrackValue(templ.Name(), result) 159 } 160 161 return result, nil 162 } 163 164 // IncludeCached executes and caches partial templates. The cache is created with name+variants as the key. 165 func (ns *Namespace) IncludeCached(name string, context interface{}, variants ...interface{}) (interface{}, error) { 166 key, err := createKey(name, variants...) 167 if err != nil { 168 return nil, err 169 } 170 171 result, err := ns.getOrCreate(key, context) 172 if err == errUnHashable { 173 // Try one more 174 key.variant = helpers.HashString(key.variant) 175 result, err = ns.getOrCreate(key, context) 176 } 177 178 return result, err 179 } 180 181 func createKey(name string, variants ...interface{}) (partialCacheKey, error) { 182 var variant interface{} 183 184 if len(variants) > 1 { 185 variant = helpers.HashString(variants...) 186 } else if len(variants) == 1 { 187 variant = variants[0] 188 t := reflect.TypeOf(variant) 189 switch t.Kind() { 190 // This isn't an exhaustive list of unhashable types. 191 // There may be structs with slices, 192 // but that should be very rare. We do recover from that situation 193 // below. 194 case reflect.Slice, reflect.Array, reflect.Map: 195 variant = helpers.HashString(variant) 196 } 197 } 198 199 return partialCacheKey{name: name, variant: variant}, nil 200 } 201 202 var errUnHashable = errors.New("unhashable") 203 204 func (ns *Namespace) getOrCreate(key partialCacheKey, context interface{}) (result interface{}, err error) { 205 defer func() { 206 if r := recover(); r != nil { 207 err = r.(error) 208 if strings.Contains(err.Error(), "unhashable type") { 209 ns.cachedPartials.RUnlock() 210 err = errUnHashable 211 } 212 } 213 }() 214 215 ns.cachedPartials.RLock() 216 p, ok := ns.cachedPartials.p[key] 217 ns.cachedPartials.RUnlock() 218 219 if ok { 220 return p, nil 221 } 222 223 p, err = ns.Include(key.name, context) 224 if err != nil { 225 return nil, err 226 } 227 228 ns.cachedPartials.Lock() 229 defer ns.cachedPartials.Unlock() 230 // Double-check. 231 if p2, ok := ns.cachedPartials.p[key]; ok { 232 return p2, nil 233 } 234 ns.cachedPartials.p[key] = p 235 236 return p, nil 237 }