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  }