hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 }