From 35fbfb19a173b01bc881f2bbc5d104136633a7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 3 Oct 2018 14:58:09 +0200 Subject: 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 --- tpl/collections/collections_test.go | 7 +- tpl/data/data.go | 6 +- tpl/data/data_test.go | 10 +-- tpl/fmt/fmt.go | 5 +- tpl/fmt/init.go | 2 +- tpl/fmt/init_test.go | 3 +- tpl/partials/init_test.go | 2 + tpl/resources/resources.go | 4 +- tpl/strings/strings.go | 8 +- tpl/template.go | 127 +++++++++++++++++++++++++++++-- tpl/template_test.go | 31 ++++++++ tpl/tplimpl/template.go | 147 ++++++++++++++++++++++-------------- tpl/tplimpl/templateProvider.go | 7 +- tpl/tplimpl/template_errors.go | 46 +++++++++++ tpl/tplimpl/template_funcs_test.go | 8 +- tpl/urls/urls.go | 7 +- 16 files changed, 327 insertions(+), 93 deletions(-) create mode 100644 tpl/template_test.go create mode 100644 tpl/tplimpl/template_errors.go (limited to 'tpl') diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index c8a7207ea..b3ac8a8f2 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -17,20 +17,17 @@ import ( "errors" "fmt" "html/template" - "io/ioutil" - "log" "math/rand" - "os" "reflect" "testing" "time" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -856,7 +853,7 @@ func newDeps(cfg config.Provider) *deps.Deps { Cfg: cfg, Fs: hugofs.NewMem(l), ContentSpec: cs, - Log: jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime), + Log: loggers.NewErrorLogger(), } } diff --git a/tpl/data/data.go b/tpl/data/data.go index 14a4975a5..3f87eda31 100644 --- a/tpl/data/data.go +++ b/tpl/data/data.go @@ -18,12 +18,12 @@ import ( "encoding/csv" "encoding/json" "errors" - "fmt" "net/http" "strings" "time" "github.com/gohugoio/hugo/deps" + _errors "github.com/pkg/errors" ) // New returns a new instance of the data-namespaced template functions. @@ -59,7 +59,7 @@ func (ns *Namespace) GetCSV(sep string, urlParts ...string) (d [][]string, err e var req *http.Request req, err = http.NewRequest("GET", url, nil) if err != nil { - return nil, fmt.Errorf("Failed to create request for getCSV for resource %s: %s", url, err) + return nil, _errors.Wrapf(err, "Failed to create request for getCSV for resource %s:", url) } req.Header.Add("Accept", "text/csv") @@ -103,7 +103,7 @@ func (ns *Namespace) GetJSON(urlParts ...string) (v interface{}, err error) { var req *http.Request req, err = http.NewRequest("GET", url, nil) if err != nil { - return nil, fmt.Errorf("Failed to create request for getJSON resource %s: %s", url, err) + return nil, _errors.Wrapf(err, "Failed to create request for getJSON resource %s:", url) } req.Header.Add("Accept", "application/json") diff --git a/tpl/data/data_test.go b/tpl/data/data_test.go index 6bee0d524..9ef969244 100644 --- a/tpl/data/data_test.go +++ b/tpl/data/data_test.go @@ -113,11 +113,11 @@ func TestGetCSV(t *testing.T) { require.NoError(t, err, msg) if _, ok := test.expect.(bool); ok { - require.Equal(t, 1, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))) + require.Equal(t, 1, int(ns.deps.Log.ErrorCounter.Count())) require.Nil(t, got) continue } - require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))) + require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count())) require.NotNil(t, got, msg) assert.EqualValues(t, test.expect, got, msg) @@ -198,14 +198,14 @@ func TestGetJSON(t *testing.T) { continue } - if errLevel, ok := test.expect.(jww.Threshold); ok { - logCount := ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(errLevel) + if errLevel, ok := test.expect.(jww.Threshold); ok && errLevel >= jww.LevelError { + logCount := ns.deps.Log.ErrorCounter.Count() require.True(t, logCount >= 1, fmt.Sprintf("got log count %d", logCount)) continue } require.NoError(t, err, msg) - require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)), msg) + require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()), msg) require.NotNil(t, got, msg) assert.EqualValues(t, test.expect, got, msg) diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go index 0f4f906c2..09e4f5a40 100644 --- a/tpl/fmt/fmt.go +++ b/tpl/fmt/fmt.go @@ -16,12 +16,13 @@ package fmt import ( _fmt "fmt" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" ) // New returns a new instance of the fmt-namespaced template functions. -func New() *Namespace { - return &Namespace{helpers.NewDistinctErrorLogger()} +func New(d *deps.Deps) *Namespace { + return &Namespace{helpers.NewDistinctLogger(d.Log.ERROR)} } // Namespace provides template functions for the "fmt" namespace. diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go index 76c68957a..117055801 100644 --- a/tpl/fmt/init.go +++ b/tpl/fmt/init.go @@ -22,7 +22,7 @@ const name = "fmt" func init() { f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { - ctx := New() + ctx := New(d) ns := &internal.TemplateFuncsNamespace{ Name: name, diff --git a/tpl/fmt/init_test.go b/tpl/fmt/init_test.go index 01eb2fa69..b693ffa2b 100644 --- a/tpl/fmt/init_test.go +++ b/tpl/fmt/init_test.go @@ -16,6 +16,7 @@ package fmt import ( "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/tpl/internal" "github.com/stretchr/testify/require" @@ -26,7 +27,7 @@ func TestInit(t *testing.T) { var ns *internal.TemplateFuncsNamespace for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{}) + ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()}) if ns.Name == name { found = true break diff --git a/tpl/partials/init_test.go b/tpl/partials/init_test.go index 4832e6b66..0513f1572 100644 --- a/tpl/partials/init_test.go +++ b/tpl/partials/init_test.go @@ -16,6 +16,7 @@ package partials import ( "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/tpl/internal" "github.com/stretchr/testify/require" @@ -28,6 +29,7 @@ func TestInit(t *testing.T) { for _, nsf := range internal.TemplateFuncsNamespaceRegistry { ns = nsf(&deps.Deps{ BuildStartListeners: &deps.Listeners{}, + Log: loggers.NewErrorLogger(), }) if ns.Name == name { found = true diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index 883afbcd7..c24cd2b42 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -18,6 +18,8 @@ import ( "fmt" "path/filepath" + _errors "github.com/pkg/errors" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource/bundler" @@ -256,7 +258,7 @@ func (ns *Namespace) resolveArgs(args []interface{}) (resource.Resource, map[str m, err := cast.ToStringMapE(args[0]) if err != nil { - return nil, nil, fmt.Errorf("invalid options type: %s", err) + return nil, nil, _errors.Wrap(err, "invalid options type") } return r, m, nil diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go index 9b8409ed6..1853cd727 100644 --- a/tpl/strings/strings.go +++ b/tpl/strings/strings.go @@ -20,6 +20,8 @@ import ( _strings "strings" "unicode/utf8" + _errors "github.com/pkg/errors" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/spf13/cast" @@ -44,7 +46,7 @@ type Namespace struct { func (ns *Namespace) CountRunes(s interface{}) (int, error) { ss, err := cast.ToStringE(s) if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err) + return 0, _errors.Wrap(err, "Failed to convert content to string") } counter := 0 @@ -61,7 +63,7 @@ func (ns *Namespace) CountRunes(s interface{}) (int, error) { func (ns *Namespace) RuneCount(s interface{}) (int, error) { ss, err := cast.ToStringE(s) if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err) + return 0, _errors.Wrap(err, "Failed to convert content to string") } return utf8.RuneCountInString(ss), nil } @@ -70,7 +72,7 @@ func (ns *Namespace) RuneCount(s interface{}) (int, error) { func (ns *Namespace) CountWords(s interface{}) (int, error) { ss, err := cast.ToStringE(s) if err != nil { - return 0, fmt.Errorf("Failed to convert content to string: %s", err) + return 0, _errors.Wrap(err, "Failed to convert content to string") } counter := 0 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 diff --git a/tpl/template_test.go b/tpl/template_test.go new file mode 100644 index 000000000..73e9640be --- /dev/null +++ b/tpl/template_test.go @@ -0,0 +1,31 @@ +// 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. +// 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 ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExtractBaseof(t *testing.T) { + assert := require.New(t) + + replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`) + + assert.Equal("_default/baseof.html", replaced) + assert.Equal("", extractBaseOf("not baseof for you")) + assert.Equal("blog/baseof.html", extractBaseOf("template: blog/baseof.html:23:11:")) + assert.Equal("blog/baseof.ace", extractBaseOf("template: blog/baseof.ace:23:11:")) +} diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index f19c312ec..fc77bb1af 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/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. @@ -20,7 +20,9 @@ import ( "strings" texttemplate "text/template" + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/tpl/tplimpl/embedded" + "github.com/pkg/errors" "github.com/eknkc/amber" @@ -64,7 +66,7 @@ type templateErr struct { } type templateLoader interface { - handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error + handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error addTemplate(name, tpl string) error addLateTemplate(name, tpl string) error } @@ -114,22 +116,11 @@ func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder { } -func (t *templateHandler) addError(name string, err error) { - t.errors = append(t.errors, &templateErr{name, err}) -} - func (t *templateHandler) Debug() { fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) } -// PrintErrors prints the accumulated errors as ERROR to the log. -func (t *templateHandler) PrintErrors() { - for _, e := range t.errors { - t.Log.ERROR.Println(e.name, ":", e.err) - } -} - // Lookup tries to find a template with the given name in both template // collections: First HTML, then the plain text template collection. func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { @@ -156,8 +147,8 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler { c := &templateHandler{ Deps: d, layoutsFs: d.BaseFs.Layouts.Fs, - html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)}, - text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template)}, + html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon}, + text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon}, errors: make([]*templateErr, 0), } @@ -187,15 +178,21 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler { } func newTemplateAdapter(deps *deps.Deps) *templateHandler { + common := &templatesCommon{ + nameBaseTemplateName: make(map[string]string), + } + htmlT := &htmlTemplates{ - t: template.New(""), - overlays: make(map[string]*template.Template), + t: template.New(""), + overlays: make(map[string]*template.Template), + templatesCommon: common, } textT := &textTemplates{ - textTemplate: &textTemplate{t: texttemplate.New("")}, - overlays: make(map[string]*texttemplate.Template), + textTemplate: &textTemplate{t: texttemplate.New("")}, + overlays: make(map[string]*texttemplate.Template), + templatesCommon: common, } - return &templateHandler{ + h := &templateHandler{ Deps: deps, layoutsFs: deps.BaseFs.Layouts.Fs, html: htmlT, @@ -203,11 +200,23 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { errors: make([]*templateErr, 0), } + common.handler = h + + return h + } -type htmlTemplates struct { +// Shared by both HTML and text templates. +type templatesCommon struct { + handler *templateHandler funcster *templateFuncster + // Used to get proper filenames in errors + nameBaseTemplateName map[string]string +} +type htmlTemplates struct { + *templatesCommon + t *template.Template // This looks, and is, strange. @@ -231,7 +240,8 @@ func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) { if templ == nil { return nil, false } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true + + return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true } func (t *htmlTemplates) lookup(name string) *template.Template { @@ -259,8 +269,8 @@ func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { } type textTemplates struct { + *templatesCommon *textTemplate - funcster *templateFuncster clone *texttemplate.Template cloneClone *texttemplate.Template @@ -272,7 +282,7 @@ func (t *textTemplates) Lookup(name string) (tpl.Template, bool) { if templ == nil { return nil, false } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true + return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true } func (t *textTemplates) lookup(name string) *texttemplate.Template { @@ -321,8 +331,8 @@ func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { // LoadTemplates loads the templates from the layouts filesystem. // A prefix can be given to indicate a template namespace to load the templates // into, i.e. "_internal" etc. -func (t *templateHandler) LoadTemplates(prefix string) { - t.loadTemplates(prefix) +func (t *templateHandler) LoadTemplates(prefix string) error { + return t.loadTemplates(prefix) } @@ -423,7 +433,6 @@ func (t *templateHandler) addLateTemplate(name, tpl string) error { func (t *templateHandler) AddLateTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addLateTemplate(name, tpl); err != nil { - t.addError(name, err) return err } return nil @@ -435,7 +444,6 @@ func (t *templateHandler) AddLateTemplate(name, tpl string) error { func (t *templateHandler) AddTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addTemplate(name, tpl); err != nil { - t.addError(name, err) return err } return nil @@ -458,14 +466,19 @@ func (t *templateHandler) MarkReady() { // RebuildClone rebuilds the cloned templates. Used for live-reloads. func (t *templateHandler) RebuildClone() { - t.html.clone = template.Must(t.html.cloneClone.Clone()) - t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) + if t.html != nil && t.html.cloneClone != nil { + t.html.clone = template.Must(t.html.cloneClone.Clone()) + } + if t.text != nil && t.text.cloneClone != nil { + t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) + } } -func (t *templateHandler) loadTemplates(prefix string) { +func (t *templateHandler) loadTemplates(prefix string) error { + walker := func(path string, fi os.FileInfo, err error) error { if err != nil || fi.IsDir() { - return nil + return err } if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { @@ -490,21 +503,25 @@ func (t *templateHandler) loadTemplates(prefix string) { tplID, err := output.CreateTemplateNames(descriptor) if err != nil { t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) - return nil } if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { - t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) + return err } return nil } if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil { - t.Log.ERROR.Printf("Failed to load templates: %s", err) + if !os.IsNotExist(err) { + return err + } + return nil } + return nil + } func (t *templateHandler) initFuncs() { @@ -553,12 +570,12 @@ func (t *templateHandler) getTemplateHandler(name string) templateLoader { return t.html } -func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { h := t.getTemplateHandler(name) return h.handleMaster(name, overlayFilename, masterFilename, onMissing) } -func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { masterTpl := t.lookup(masterFilename) @@ -568,9 +585,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - masterTpl, err = t.t.New(overlayFilename).Parse(templ) + masterTpl, err = t.t.New(overlayFilename).Parse(templ.template) if err != nil { - return err + return templ.errWithFileContext("parse master failed", err) } } @@ -579,9 +596,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template) if err != nil { - return err + return templ.errWithFileContext("parse failed", err) } // The extra lookup is a workaround, see @@ -593,12 +610,13 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin } t.overlays[name] = overlayTpl + t.nameBaseTemplateName[name] = masterFilename return err } -func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { name = strings.TrimPrefix(name, textTmplNamePrefix) masterTpl := t.lookup(masterFilename) @@ -609,10 +627,11 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - masterTpl, err = t.t.New(overlayFilename).Parse(templ) + masterTpl, err = t.t.New(masterFilename).Parse(templ.template) if err != nil { - return err + return errors.Wrapf(err, "failed to parse %q:", templ.filename) } + t.nameBaseTemplateName[masterFilename] = templ.filename } templ, err := onMissing(overlayFilename) @@ -620,9 +639,9 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) + overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template) if err != nil { - return err + return errors.Wrapf(err, "failed to parse %q:", templ.filename) } overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) @@ -630,6 +649,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } t.overlays[name] = overlayTpl + t.nameBaseTemplateName[name] = templ.filename return err @@ -640,14 +660,22 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e t.Log.DEBUG.Printf("Add template file: name %q, baseTemplatePath %q, path %q", name, baseTemplatePath, path) - getTemplate := func(filename string) (string, error) { - b, err := afero.ReadFile(t.Layouts.Fs, filename) + getTemplate := func(filename string) (templateInfo, error) { + fs := t.Layouts.Fs + b, err := afero.ReadFile(fs, filename) if err != nil { - return "", err + return templateInfo{filename: filename, fs: fs}, err } s := string(b) - return s, nil + realFilename := filename + if fi, err := fs.Stat(filename); err == nil { + if fir, ok := fi.(hugofs.RealFilenameInfo); ok { + realFilename = fir.RealFilename() + } + } + + return templateInfo{template: s, filename: filename, realFilename: realFilename, fs: fs}, nil } // get the suffix and switch on that @@ -712,7 +740,11 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e return err } - return t.AddTemplate(name, templ) + err = t.AddTemplate(name, templ.template) + if err != nil { + return templ.errWithFileContext("parse failed", err) + } + return nil } } @@ -720,19 +752,24 @@ var embeddedTemplatesAliases = map[string][]string{ "shortcodes/twitter.html": []string{"shortcodes/tweet.html"}, } -func (t *templateHandler) loadEmbedded() { +func (t *templateHandler) loadEmbedded() error { for _, kv := range embedded.EmbeddedTemplates { - // TODO(bep) error handling name, templ := kv[0], kv[1] - t.addInternalTemplate(name, templ) + if err := t.addInternalTemplate(name, templ); err != nil { + return err + } if aliases, found := embeddedTemplatesAliases[name]; found { for _, alias := range aliases { - t.addInternalTemplate(alias, templ) + if err := t.addInternalTemplate(alias, templ); err != nil { + return err + } } } } + return nil + } func (t *templateHandler) addInternalTemplate(name, tpl string) error { diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go index df44e81a6..3a803f2da 100644 --- a/tpl/tplimpl/templateProvider.go +++ b/tpl/tplimpl/templateProvider.go @@ -33,12 +33,15 @@ func (*TemplateProvider) Update(deps *deps.Deps) error { deps.TextTmpl = newTmpl.NewTextTemplate() newTmpl.initFuncs() - newTmpl.loadEmbedded() + + if err := newTmpl.loadEmbedded(); err != nil { + return err + } if deps.WithTemplate != nil { err := deps.WithTemplate(newTmpl) if err != nil { - newTmpl.addError("init", err) + return err } } diff --git a/tpl/tplimpl/template_errors.go b/tpl/tplimpl/template_errors.go new file mode 100644 index 000000000..a422d77f1 --- /dev/null +++ b/tpl/tplimpl/template_errors.go @@ -0,0 +1,46 @@ +// 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. +// 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 tplimpl + +import ( + "github.com/gohugoio/hugo/common/herrors" + "github.com/pkg/errors" + "github.com/spf13/afero" +) + +type templateInfo struct { + template string + + // Used to create some error context in error situations + fs afero.Fs + + // The filename relative to the fs above. + filename string + + // The real filename (if possible). Used for logging. + realFilename string +} + +func (info templateInfo) errWithFileContext(what string, err error) error { + err = errors.Wrapf(err, "file %q: %s:", info.realFilename, what) + + err, _ = herrors.WithFileContextForFile( + err, + info.filename, + info.fs, + "go-html-template", + herrors.SimpleLineMatcher) + + return err +} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 8594c67a4..04bb4941a 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -21,10 +21,7 @@ import ( "testing" "time" - "io/ioutil" - "log" - "os" - + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" @@ -35,13 +32,12 @@ import ( "github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/partials" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) var ( - logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + logger = loggers.NewErrorLogger() ) func newTestConfig() config.Provider { diff --git a/tpl/urls/urls.go b/tpl/urls/urls.go index 54c94ec17..8f6f92f3d 100644 --- a/tpl/urls/urls.go +++ b/tpl/urls/urls.go @@ -17,11 +17,12 @@ import ( "errors" "fmt" - "github.com/russross/blackfriday" - "html/template" "net/url" + _errors "github.com/pkg/errors" + "github.com/russross/blackfriday" + "github.com/gohugoio/hugo/deps" "github.com/spf13/cast" ) @@ -55,7 +56,7 @@ func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) { func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) { s, err := cast.ToStringE(rawurl) if err != nil { - return nil, fmt.Errorf("Error in Parse: %s", err) + return nil, _errors.Wrap(err, "Error in Parse") } return url.Parse(s) -- cgit v1.2.3