diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-10-03 15:58:09 +0300 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2018-10-16 23:10:56 +0300 |
commit | 35fbfb19a173b01bc881f2bbc5d104136633a7ec (patch) | |
tree | 636d0d51fa262dc808eb3c5cc9cf92ad977a0c6a /tpl/template.go | |
parent | 3a3089121b852332b5744d1f566959c8cf93cef4 (diff) |
commands: Show server error info in browser
The main item in this commit is showing of errors with a file context when running `hugo server`.
This can be turned off: `hugo server --disableBrowserError` (can also be set in `config.toml`).
But to get there, the error handling in Hugo needed a revision. There are some items left TODO for commits soon to follow, most notable errors in content and config files.
Fixes #5284
Fixes #5290
See #5325
See #5324
Diffstat (limited to 'tpl/template.go')
-rw-r--r-- | tpl/template.go | 127 |
1 files changed, 121 insertions, 6 deletions
diff --git a/tpl/template.go b/tpl/template.go index 2cef92bb2..02b2d4a9b 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -1,4 +1,4 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. +// Copyright 2018 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. @@ -14,16 +14,26 @@ package tpl import ( + "fmt" "io" + "path/filepath" + "regexp" + "strings" "time" - "text/template/parse" + "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 ( @@ -35,8 +45,7 @@ type TemplateHandler interface { TemplateFinder AddTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error - LoadTemplates(prefix string) - PrintErrors() + LoadTemplates(prefix string) error NewTextTemplate() TemplateParseFinder @@ -82,16 +91,122 @@ type TemplateDebugger interface { type TemplateAdapter struct { Template Metrics metrics.Provider + + // 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{}) error { +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.) + if r := recover(); r != nil { + execErr = t.addFileContext(t.Name(), fmt.Errorf("panic in Execute: %s", r)) + } + }() + if t.Metrics != nil { defer t.Metrics.MeasureSince(t.Name(), time.Now()) } - return t.Template.Execute(w, data) + + execErr = t.Template.Execute(w, data) + if execErr != nil { + execErr = t.addFileContext(t.Name(), execErr) + } + + return +} + +var identifiersRe = regexp.MustCompile("at \\<(.*?)\\>:") + +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 { + f, realFilename, err := t.fileAndFilename(t.Name()) + if err != nil { + return err + } + defer f.Close() + + master, hasMaster := t.NameBaseTemplateName[name] + + ferr := errors.Wrapf(inerr, "execute of template %q failed", realFilename) + + // 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(le herrors.FileError, lineNumber int, line string) bool { + if le.LineNumber() != lineNumber { + return false + } + if !hasMaster { + return true + } + + identifiers := t.extractIdentifiers(le.Error()) + + for _, id := range identifiers { + if strings.Contains(line, id) { + return true + } + } + return false + } + + // TODO(bep) 2errors text vs HTML + fe, ok := herrors.WithFileContext(ferr, f, "go-html-template", 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() + + ferr = errors.Wrapf(inerr, "execute of template %q failed", realFilename) + fe, _ = herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher) + 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, "", errors.Wrapf(err, "failed to Stat %q", filename) + } + f, err := fs.Open(filename) + if err != nil { + return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename) + } + + return f, fi.(hugofs.RealFilenameInfo).RealFilename(), nil } // ExecuteToString executes the current template and returns the result as a |