github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  	"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 interface{}
    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]interface{}
    60  }
    61  
    62  func (p *partialCache) clear() {
    63  	p.Lock()
    64  	defer p.Unlock()
    65  	p.p = make(map[partialCacheKey]interface{})
    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]interface{})}
    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    interface{}
    91  	Result interface{}
    92  }
    93  
    94  // Set sets the return value and returns an empty string.
    95  func (c *contextWrapper) Set(in interface{}) 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 ...interface{}) (interface{}, 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 ...interface{}) (string, interface{}, error) {
   121  	var data interface{}
   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 interface{}
   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 interface{}, variants ...interface{}) (interface{}, 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 ...interface{}) (partialCacheKey, error) {
   202  	var variant interface{}
   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 interface{}) (result interface{}, 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  	// We may already have a write lock.
   237  	hasLock := tpl.GetHasLockFromContext(ctx)
   238  
   239  	if !hasLock {
   240  		ns.cachedPartials.RLock()
   241  	}
   242  	p, ok := ns.cachedPartials.p[key]
   243  	if !hasLock {
   244  		ns.cachedPartials.RUnlock()
   245  	}
   246  
   247  	if ok {
   248  		if ns.deps.Metrics != nil {
   249  			ns.deps.Metrics.TrackValue(key.templateName(), p, true)
   250  			// The templates that gets executed is measued in Execute.
   251  			// We need to track the time spent in the cache to
   252  			// get the totals correct.
   253  			ns.deps.Metrics.MeasureSince(key.templateName(), start)
   254  
   255  		}
   256  		return p, nil
   257  	}
   258  
   259  	if !hasLock {
   260  		ns.cachedPartials.Lock()
   261  		defer ns.cachedPartials.Unlock()
   262  		ctx = tpl.SetHasLockInContext(ctx, true)
   263  	}
   264  
   265  	var name string
   266  	name, p, err = ns.include(ctx, key.name, context)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  
   271  	if ns.deps.Metrics != nil {
   272  		ns.deps.Metrics.TrackValue(name, p, false)
   273  	}
   274  	ns.cachedPartials.p[key] = p
   275  
   276  	return p, nil
   277  }