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  }