github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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 "context" 20 "fmt" 21 "html/template" 22 "io" 23 "strings" 24 "time" 25 26 "github.com/bep/lazycache" 27 28 "github.com/gohugoio/hugo/identity" 29 30 texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" 31 32 "github.com/gohugoio/hugo/tpl" 33 34 bp "github.com/gohugoio/hugo/bufferpool" 35 "github.com/gohugoio/hugo/deps" 36 ) 37 38 type partialCacheKey struct { 39 Name string 40 Variants []any 41 } 42 type includeResult struct { 43 name string 44 result any 45 err error 46 } 47 48 func (k partialCacheKey) Key() string { 49 if k.Variants == nil { 50 return k.Name 51 } 52 return identity.HashString(append([]any{k.Name}, k.Variants...)...) 53 } 54 55 func (k partialCacheKey) templateName() string { 56 if !strings.HasPrefix(k.Name, "partials/") { 57 return "partials/" + k.Name 58 } 59 return k.Name 60 } 61 62 // partialCache represents a LRU cache of partials. 63 type partialCache struct { 64 cache *lazycache.Cache[string, includeResult] 65 } 66 67 func (p *partialCache) clear() { 68 p.cache.DeleteFunc(func(string, includeResult) bool { 69 return true 70 }) 71 } 72 73 // New returns a new instance of the templates-namespaced template functions. 74 func New(deps *deps.Deps) *Namespace { 75 // This lazycache was introduced in Hugo 0.111.0. 76 // We're going to expand and consolidate all memory caches in Hugo using this, 77 // so just set a high limit for now. 78 lru := lazycache.New[string, includeResult](lazycache.Options{MaxEntries: 1000}) 79 80 cache := &partialCache{cache: lru} 81 deps.BuildStartListeners.Add( 82 func() { 83 cache.clear() 84 }) 85 86 return &Namespace{ 87 deps: deps, 88 cachedPartials: cache, 89 } 90 } 91 92 // Namespace provides template functions for the "templates" namespace. 93 type Namespace struct { 94 deps *deps.Deps 95 cachedPartials *partialCache 96 } 97 98 // contextWrapper makes room for a return value in a partial invocation. 99 type contextWrapper struct { 100 Arg any 101 Result any 102 } 103 104 // Set sets the return value and returns an empty string. 105 func (c *contextWrapper) Set(in any) string { 106 c.Result = in 107 return "" 108 } 109 110 // Include executes the named partial. 111 // If the partial contains a return statement, that value will be returned. 112 // Else, the rendered output will be returned: 113 // A string if the partial is a text/template, or template.HTML when html/template. 114 // Note that ctx is provided by Hugo, not the end user. 115 func (ns *Namespace) Include(ctx context.Context, name string, contextList ...any) (any, error) { 116 res := ns.includWithTimeout(ctx, name, contextList...) 117 if res.err != nil { 118 return nil, res.err 119 } 120 121 if ns.deps.Metrics != nil { 122 ns.deps.Metrics.TrackValue(res.name, res.result, false) 123 } 124 125 return res.result, nil 126 } 127 128 func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult { 129 // Create a new context with a timeout not connected to the incoming context. 130 timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout()) 131 defer cancel() 132 133 res := make(chan includeResult, 1) 134 135 go func() { 136 res <- ns.include(ctx, name, dataList...) 137 }() 138 139 select { 140 case r := <-res: 141 return r 142 case <-timeoutCtx.Done(): 143 err := timeoutCtx.Err() 144 if err == context.DeadlineExceeded { 145 err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Conf.Timeout()) 146 } 147 return includeResult{err: err} 148 } 149 150 } 151 152 // include is a helper function that lookups and executes the named partial. 153 // Returns the final template name and the rendered output. 154 func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult { 155 var data any 156 if len(dataList) > 0 { 157 data = dataList[0] 158 } 159 160 var n string 161 if strings.HasPrefix(name, "partials/") { 162 n = name 163 } else { 164 n = "partials/" + name 165 } 166 167 templ, found := ns.deps.Tmpl().Lookup(n) 168 if !found { 169 // For legacy reasons. 170 templ, found = ns.deps.Tmpl().Lookup(n + ".html") 171 } 172 173 if !found { 174 return includeResult{err: fmt.Errorf("partial %q not found", name)} 175 } 176 177 var info tpl.ParseInfo 178 if ip, ok := templ.(tpl.Info); ok { 179 info = ip.ParseInfo() 180 } 181 182 var w io.Writer 183 184 if info.HasReturn { 185 // Wrap the context sent to the template to capture the return value. 186 // Note that the template is rewritten to make sure that the dot (".") 187 // and the $ variable points to Arg. 188 data = &contextWrapper{ 189 Arg: data, 190 } 191 192 // We don't care about any template output. 193 w = io.Discard 194 } else { 195 b := bp.GetBuffer() 196 defer bp.PutBuffer(b) 197 w = b 198 } 199 200 if err := ns.deps.Tmpl().ExecuteWithContext(ctx, templ, w, data); err != nil { 201 return includeResult{err: err} 202 } 203 204 var result any 205 206 if ctx, ok := data.(*contextWrapper); ok { 207 result = ctx.Result 208 } else if _, ok := templ.(*texttemplate.Template); ok { 209 result = w.(fmt.Stringer).String() 210 } else { 211 result = template.HTML(w.(fmt.Stringer).String()) 212 } 213 214 return includeResult{ 215 name: templ.Name(), 216 result: result, 217 } 218 219 } 220 221 // IncludeCached executes and caches partial templates. The cache is created with name+variants as the key. 222 // Note that ctx is provided by Hugo, not the end user. 223 func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any, variants ...any) (any, error) { 224 start := time.Now() 225 key := partialCacheKey{ 226 Name: name, 227 Variants: variants, 228 } 229 230 r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) { 231 r := ns.includWithTimeout(ctx, key.Name, context) 232 return r, r.err 233 }) 234 235 if err != nil { 236 return nil, err 237 } 238 239 if ns.deps.Metrics != nil { 240 if found { 241 // The templates that gets executed is measured in Execute. 242 // We need to track the time spent in the cache to 243 // get the totals correct. 244 ns.deps.Metrics.MeasureSince(key.templateName(), start) 245 246 } 247 ns.deps.Metrics.TrackValue(key.templateName(), r.result, found) 248 } 249 250 return r.result, nil 251 }