// Copyright 2019 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tpl import ( "fmt" "io" "path/filepath" "regexp" "strings" "time" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" "html/template" texttemplate "text/template" "text/template/parse" bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/metrics" "github.com/pkg/errors" ) var ( _ TemplateExecutor = (*TemplateAdapter)(nil) _ TemplateInfoProvider = (*TemplateAdapter)(nil) ) // TemplateHandler manages the collection of templates. type TemplateHandler interface { TemplateFinder AddTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error LoadTemplates(prefix string) error NewTextTemplate() TemplateParseFinder MarkReady() error RebuildClone() } // TemplateVariants describes the possible variants of a template. // All of these may be empty. type TemplateVariants struct { Language string OutputFormat output.Format } // TemplateFinder finds templates. type TemplateFinder interface { TemplateLookup TemplateLookupVariant } type TemplateLookup interface { Lookup(name string) (Template, bool) } type TemplateLookupVariant interface { // TODO(bep) this currently only works for shortcodes. // We may unify and expand this variant pattern to the // other templates, but we need this now for the shortcodes to // quickly determine if a shortcode has a template for a given // output format. // It returns the template, if it was found or not and if there are // alternative representations (output format, language). // We are currently only interested in output formats, so we should improve // this for speed. LookupVariant(name string, variants TemplateVariants) (Template, bool, bool) } // Template is the common interface between text/template and html/template. type Template interface { Execute(wr io.Writer, data interface{}) error Name() string } // TemplateInfoProvider provides some contextual information about a template. type TemplateInfoProvider interface { TemplateInfo() Info } // TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain. type TemplateParser interface { Parse(name, tpl string) (Template, error) } // TemplateParseFinder provides both parsing and finding. type TemplateParseFinder interface { TemplateParser TemplateFinder } // TemplateExecutor adds some extras to Template. type TemplateExecutor interface { Template ExecuteToString(data interface{}) (string, error) Tree() string } // TemplateDebugger prints some debug info to stdoud. type TemplateDebugger interface { Debug() } // TemplateAdapter implements the TemplateExecutor interface. type TemplateAdapter struct { Template Metrics metrics.Provider Info Info // The filesystem where the templates are stored. Fs afero.Fs // Maps to base template if relevant. NameBaseTemplateName map[string]string } var baseOfRe = regexp.MustCompile("template: (.*?):") func extractBaseOf(err string) string { m := baseOfRe.FindStringSubmatch(err) if len(m) == 2 { return m[1] } return "" } // Execute executes the current template. The actual execution is performed // by the embedded text or html template, but we add an implementation here so // we can add a timer for some metrics. func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) { defer func() { // Panics in templates are a little bit too common (nil pointers etc.) // See https://github.com/gohugoio/hugo/issues/5327 if r := recover(); r != nil { execErr = t.addFileContext(t.Name(), fmt.Errorf(`panic in Execute: %s. See "https://github.com/gohugoio/hugo/issues/5327" for the reason why we cannot provide a better error message for this`, r)) } }() if t.Metrics != nil { defer t.Metrics.MeasureSince(t.Name(), time.Now()) } execErr = t.Template.Execute(w, data) if execErr != nil { execErr = t.addFileContext(t.Name(), execErr) } return } func (t *TemplateAdapter) TemplateInfo() Info { return t.Info } // The identifiers may be truncated in the log, e.g. // "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image" var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`) func (t *TemplateAdapter) extractIdentifiers(line string) []string { m := identifiersRe.FindAllStringSubmatch(line, -1) identifiers := make([]string, len(m)) for i := 0; i < len(m); i++ { identifiers[i] = m[i][1] } return identifiers } func (t *TemplateAdapter) addFileContext(name string, inerr error) error { if strings.HasPrefix(t.Name(), "_internal") { return inerr } f, realFilename, err := t.fileAndFilename(t.Name()) if err != nil { return inerr } defer f.Close() master, hasMaster := t.NameBaseTemplateName[name] ferr := errors.Wrap(inerr, "execute of template failed") // Since this can be a composite of multiple template files (single.html + baseof.html etc.) // we potentially need to look in both -- and cannot rely on line number alone. lineMatcher := func(m herrors.LineMatcher) bool { if m.Position.LineNumber != m.LineNumber { return false } if !hasMaster { return true } identifiers := t.extractIdentifiers(m.Error.Error()) for _, id := range identifiers { if strings.Contains(m.Line, id) { return true } } return false } fe, ok := herrors.WithFileContext(ferr, realFilename, f, lineMatcher) if ok || !hasMaster { return fe } // Try the base template if relevant f, realFilename, err = t.fileAndFilename(master) if err != nil { return err } defer f.Close() fe, ok = herrors.WithFileContext(ferr, realFilename, f, lineMatcher) if !ok { // Return the most specific. return ferr } return fe } func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) { fs := t.Fs filename := filepath.FromSlash(name) fi, err := fs.Stat(filename) if err != nil { return nil, "", err } fim := fi.(hugofs.FileMetaInfo) meta := fim.Meta() f, err := meta.Open() if err != nil { return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename) } return f, meta.Filename(), nil } // ExecuteToString executes the current template and returns the result as a // string. func (t *TemplateAdapter) ExecuteToString(data interface{}) (string, error) { b := bp.GetBuffer() defer bp.PutBuffer(b) if err := t.Execute(b, data); err != nil { return "", err } return b.String(), nil } // Tree returns the template Parse tree as a string. // Note: this isn't safe for parallel execution on the same template // vs Lookup and Execute. func (t *TemplateAdapter) Tree() string { var tree *parse.Tree switch tt := t.Template.(type) { case *template.Template: tree = tt.Tree case *texttemplate.Template: tree = tt.Tree default: panic("Unknown template") } if tree == nil || tree.Root == nil { return "" } s := tree.Root.String() return s } // TemplateFuncsGetter allows to get a map of functions. type TemplateFuncsGetter interface { GetFuncs() map[string]interface{} } // TemplateTestMocker adds a way to override some template funcs during tests. // The interface is named so it's not used in regular application code. type TemplateTestMocker interface { SetFuncs(funcMap map[string]interface{}) }