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
This commit is contained in:
Bjørn Erik Pedersen
2018-10-03 14:58:09 +02:00
parent 3a3089121b
commit 35fbfb19a1
73 changed files with 1914 additions and 668 deletions

View File

@@ -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(),
}
}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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.

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

31
tpl/template_test.go Normal file
View File

@@ -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:"))
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)